Commit 57d9cf5d5c3f1392fcbd3ec97bdb4888b5b1c697
1 parent
17bd7d21
feat(db): 22 张 vk_ 业务表与认领计数列
- 新包 @voko/exchange、@voko/clearing(当前只有表结构),@voko/access 增 3 张表 - migration 202610100001 建表,202610100002 给 vk_access_requests 加 claimed_agent_count(≤3) - 稀有度为 1–10 的整数等级 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Showing
37 changed files
with
898 additions
and
1 deletions
apps/voko-server/package.json
| ... | ... | @@ -23,6 +23,8 @@ |
| 23 | 23 | "@drama/observability": "workspace:*", |
| 24 | 24 | "@drama/server-kit": "workspace:*", |
| 25 | 25 | "@voko/access": "workspace:*", |
| 26 | + "@voko/clearing": "workspace:*", | |
| 27 | + "@voko/exchange": "workspace:*", | |
| 26 | 28 | "dotenv": "16.6.1", |
| 27 | 29 | "fastify": "^5.1.0", |
| 28 | 30 | "googleapis": "^173.0.0", | ... | ... |
apps/voko-server/src/db/migrations/202610100001_vk_core_tables.js
0 → 100644
| 1 | +/** Alpha 的 22 张 vk_ 业务表(SDD「数据模型」);与各包 tables/*.cjs 的当前结构逐字一致。 */ | |
| 2 | +export async function up(db) { | |
| 3 | + await db.schema.createTable('vk_guilds', (t) => { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('name', 120).notNullable() | |
| 6 | + t.string('slug', 64).notNullable() | |
| 7 | + t.string('lead_user_id', 64).notNullable() | |
| 8 | + t.string('company_agent_id', 64).nullable() | |
| 9 | + t.boolean('require_lead_approval').notNullable() | |
| 10 | + t.integer('checkpoint_interval_sec').unsigned().notNullable().defaultTo(21600) | |
| 11 | + t.integer('piece_review_window_sec').unsigned().notNullable().defaultTo(86400) | |
| 12 | + t.integer('contest_review_window_sec').unsigned().notNullable().defaultTo(259200) | |
| 13 | + t.integer('transfer_ttl_sec').unsigned().notNullable().defaultTo(86400) | |
| 14 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 15 | + t.datetime('updated_at', { precision: 3 }).nullable() | |
| 16 | + t.unique(['slug'], { indexName: 'uq_vk_guilds_slug' }) | |
| 17 | + t.check('checkpoint_interval_sec > 0 and piece_review_window_sec > 0 and contest_review_window_sec > 0 and transfer_ttl_sec > 0', [], 'ck_vk_guilds_windows') | |
| 18 | + }) | |
| 19 | + | |
| 20 | + await db.schema.createTable('vk_guild_members', (t) => { | |
| 21 | + t.string('id', 64).primary() | |
| 22 | + t.string('guild_id', 64).notNullable() | |
| 23 | + t.string('user_id', 64).notNullable() | |
| 24 | + t.string('role', 16).notNullable() | |
| 25 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 26 | + t.unique(['guild_id', 'user_id'], { indexName: 'uq_vk_guild_members_guild_user' }) | |
| 27 | + t.index(['user_id'], 'idx_vk_guild_members_user') | |
| 28 | + t.check("role in ('lead','member')", [], 'ck_vk_guild_members_role') | |
| 29 | + }) | |
| 30 | + | |
| 31 | + await db.schema.createTable('vk_agents', (t) => { | |
| 32 | + t.string('id', 64).primary() | |
| 33 | + t.string('wallet_user_id', 64).notNullable() | |
| 34 | + t.string('owner_user_id', 64).nullable() | |
| 35 | + t.string('guild_id', 64).nullable() | |
| 36 | + t.string('role', 16).notNullable() | |
| 37 | + t.string('status', 16).notNullable() | |
| 38 | + t.string('name', 120).notNullable() | |
| 39 | + t.string('description', 1000).nullable() | |
| 40 | + t.json('skills').notNullable() | |
| 41 | + t.string('owner_email', 320).nullable() | |
| 42 | + t.integer('level').unsigned().notNullable() | |
| 43 | + t.bigInteger('xp').unsigned().notNullable() | |
| 44 | + t.string('webhook_url', 2048).nullable() | |
| 45 | + t.text('webhook_secret_enc').nullable() | |
| 46 | + t.bigInteger('daily_spend_limit').nullable() | |
| 47 | + t.bigInteger('events_cursor').unsigned().notNullable() | |
| 48 | + t.datetime('claimed_at', { precision: 3 }).nullable() | |
| 49 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 50 | + t.datetime('updated_at', { precision: 3 }).nullable() | |
| 51 | + t.unique(['wallet_user_id'], { indexName: 'uq_vk_agents_wallet' }) | |
| 52 | + t.index(['owner_user_id'], 'idx_vk_agents_owner') | |
| 53 | + t.index(['guild_id', 'status'], 'idx_vk_agents_guild_status') | |
| 54 | + t.check("role in ('company','member')", [], 'ck_vk_agents_role') | |
| 55 | + t.check("status in ('pending_claim','active','suspended')", [], 'ck_vk_agents_status') | |
| 56 | + t.check("(status = 'pending_claim' and owner_user_id is null and claimed_at is null and guild_id is null) or (status <> 'pending_claim' and owner_user_id is not null and claimed_at is not null and guild_id is not null)", [], 'ck_vk_agents_claim') | |
| 57 | + t.check('(webhook_url is null) = (webhook_secret_enc is null)', [], 'ck_vk_agents_webhook_pair') | |
| 58 | + t.check('level >= 1', [], 'ck_vk_agents_level') | |
| 59 | + t.check('daily_spend_limit is null or daily_spend_limit >= 0', [], 'ck_vk_agents_daily_spend_limit') | |
| 60 | + }) | |
| 61 | + | |
| 62 | + await db.schema.createTable('vk_tasks', (t) => { | |
| 63 | + t.string('id', 64).primary() | |
| 64 | + t.string('guild_id', 64).notNullable() | |
| 65 | + t.string('publisher_agent_id', 64).notNullable() | |
| 66 | + t.string('template', 16).notNullable() | |
| 67 | + t.string('title', 200).notNullable() | |
| 68 | + t.text('spec').notNullable() | |
| 69 | + t.bigInteger('reward').notNullable() | |
| 70 | + t.tinyint('rarity').unsigned().notNullable() | |
| 71 | + t.datetime('deadline_at', { precision: 3 }).notNullable() | |
| 72 | + t.string('status', 24).notNullable() | |
| 73 | + t.string('client_request_key', 191).notNullable() | |
| 74 | + t.string('holder_agent_id', 64).nullable() | |
| 75 | + t.datetime('next_checkpoint_due_at', { precision: 3 }).nullable() | |
| 76 | + t.tinyint('missed_checkpoints').unsigned().notNullable() | |
| 77 | + t.tinyint('rework_count').unsigned().notNullable() | |
| 78 | + t.datetime('review_due_at', { precision: 3 }).nullable() | |
| 79 | + t.datetime('settled_at', { precision: 3 }).nullable() | |
| 80 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 81 | + t.datetime('updated_at', { precision: 3 }).nullable() | |
| 82 | + t.unique(['publisher_agent_id', 'client_request_key'], { indexName: 'uq_vk_tasks_publisher_request' }) | |
| 83 | + t.index(['guild_id', 'status', 'deadline_at'], 'idx_vk_tasks_board') | |
| 84 | + t.index(['publisher_agent_id', 'status'], 'idx_vk_tasks_publisher_status') | |
| 85 | + t.index(['holder_agent_id', 'status'], 'idx_vk_tasks_holder_status') | |
| 86 | + t.check("template in ('piecework','contest')", [], 'ck_vk_tasks_template') | |
| 87 | + t.check("status in ('pending_approval','open','claimed','in_contest','submitted','reviewing','rework','disputed','settled','cancelled','refunded')", [], 'ck_vk_tasks_status') | |
| 88 | + t.check('reward > 0', [], 'ck_vk_tasks_reward') | |
| 89 | + t.check('rarity between 1 and 10', [], 'ck_vk_tasks_rarity') | |
| 90 | + t.check('rework_count <= 2', [], 'ck_vk_tasks_rework_count') | |
| 91 | + t.check("status not in ('claimed','submitted','rework') or template = 'piecework'", [], 'ck_vk_tasks_piecework_status') | |
| 92 | + t.check("status not in ('in_contest','reviewing') or template = 'contest'", [], 'ck_vk_tasks_contest_status') | |
| 93 | + t.check("template <> 'contest' or holder_agent_id is null", [], 'ck_vk_tasks_contest_holder') | |
| 94 | + t.check("status <> 'claimed' or (holder_agent_id is not null and next_checkpoint_due_at is not null)", [], 'ck_vk_tasks_claimed') | |
| 95 | + t.check("status not in ('submitted','reviewing') or review_due_at is not null", [], 'ck_vk_tasks_review_due') | |
| 96 | + t.check("status <> 'settled' or settled_at is not null", [], 'ck_vk_tasks_settled') | |
| 97 | + }) | |
| 98 | + | |
| 99 | + await db.schema.createTable('vk_task_acceptance_items', (t) => { | |
| 100 | + t.string('task_id', 64).notNullable() | |
| 101 | + t.string('item_key', 32).notNullable() | |
| 102 | + t.tinyint('ordinal').unsigned().notNullable() | |
| 103 | + t.string('text', 1000).notNullable() | |
| 104 | + t.primary(['task_id', 'item_key']) | |
| 105 | + t.unique(['task_id', 'ordinal'], { indexName: 'uq_vk_task_acceptance_items_ordinal' }) | |
| 106 | + t.check('ordinal between 1 and 5', [], 'ck_vk_task_acceptance_items_ordinal') | |
| 107 | + }) | |
| 108 | + | |
| 109 | + await db.schema.createTable('vk_task_holds', (t) => { | |
| 110 | + t.string('id', 64).primary() | |
| 111 | + t.string('task_id', 64).notNullable() | |
| 112 | + t.string('agent_id', 64).notNullable() | |
| 113 | + t.datetime('started_at', { precision: 3 }).notNullable() | |
| 114 | + t.datetime('ended_at', { precision: 3 }).nullable() | |
| 115 | + t.string('end_reason', 16).nullable() | |
| 116 | + t.boolean('has_valid_checkpoint').notNullable() | |
| 117 | + t.specificType('open_task_id', 'varchar(64) GENERATED ALWAYS AS (CASE WHEN ended_at IS NULL THEN task_id ELSE NULL END) STORED') | |
| 118 | + t.unique(['open_task_id'], { indexName: 'uq_vk_task_holds_open_task' }) | |
| 119 | + t.index(['task_id', 'started_at'], 'idx_vk_task_holds_task') | |
| 120 | + t.index(['agent_id', 'ended_at'], 'idx_vk_task_holds_agent') | |
| 121 | + t.check("end_reason in ('submitted','released','timeout','transferred')", [], 'ck_vk_task_holds_end_reason') | |
| 122 | + t.check('(ended_at is null) = (end_reason is null)', [], 'ck_vk_task_holds_end_pair') | |
| 123 | + }) | |
| 124 | + | |
| 125 | + await db.schema.createTable('vk_task_entries', (t) => { | |
| 126 | + t.string('id', 64).primary() | |
| 127 | + t.string('task_id', 64).notNullable() | |
| 128 | + t.string('agent_id', 64).notNullable() | |
| 129 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 130 | + t.unique(['task_id', 'agent_id'], { indexName: 'uq_vk_task_entries_task_agent' }) | |
| 131 | + t.index(['agent_id'], 'idx_vk_task_entries_agent') | |
| 132 | + }) | |
| 133 | + | |
| 134 | + await db.schema.createTable('vk_checkpoints', (t) => { | |
| 135 | + t.string('id', 64).primary() | |
| 136 | + t.string('task_id', 64).notNullable() | |
| 137 | + t.string('hold_id', 64).notNullable() | |
| 138 | + t.string('agent_id', 64).notNullable() | |
| 139 | + t.tinyint('progress').unsigned().notNullable() | |
| 140 | + t.string('note', 2000).nullable() | |
| 141 | + t.json('links').notNullable() | |
| 142 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 143 | + t.index(['task_id', 'created_at'], 'idx_vk_checkpoints_task') | |
| 144 | + t.index(['hold_id'], 'idx_vk_checkpoints_hold') | |
| 145 | + t.check('progress <= 100', [], 'ck_vk_checkpoints_progress') | |
| 146 | + }) | |
| 147 | + | |
| 148 | + await db.schema.createTable('vk_submissions', (t) => { | |
| 149 | + t.string('id', 64).primary() | |
| 150 | + t.string('task_id', 64).notNullable() | |
| 151 | + t.string('agent_id', 64).notNullable() | |
| 152 | + t.tinyint('round').unsigned().notNullable() | |
| 153 | + t.text('summary').notNullable() | |
| 154 | + t.json('artifacts').notNullable() | |
| 155 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 156 | + t.unique(['task_id', 'agent_id', 'round'], { indexName: 'uq_vk_submissions_task_agent_round' }) | |
| 157 | + t.check('round between 1 and 3', [], 'ck_vk_submissions_round') | |
| 158 | + }) | |
| 159 | + | |
| 160 | + await db.schema.createTable('vk_reviews', (t) => { | |
| 161 | + t.string('id', 64).primary() | |
| 162 | + t.string('task_id', 64).notNullable() | |
| 163 | + t.string('submission_id', 64).notNullable() | |
| 164 | + t.string('reviewer_kind', 16).notNullable() | |
| 165 | + t.string('action', 16).notNullable() | |
| 166 | + t.text('reason').nullable() | |
| 167 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 168 | + t.index(['task_id', 'created_at'], 'idx_vk_reviews_task') | |
| 169 | + t.index(['submission_id'], 'idx_vk_reviews_submission') | |
| 170 | + t.check("reviewer_kind in ('publisher','auto','arbitration')", [], 'ck_vk_reviews_reviewer_kind') | |
| 171 | + t.check("action in ('accept','reject','select_winner')", [], 'ck_vk_reviews_action') | |
| 172 | + }) | |
| 173 | + | |
| 174 | + await db.schema.createTable('vk_review_checks', (t) => { | |
| 175 | + t.string('review_id', 64).notNullable() | |
| 176 | + t.string('item_key', 32).notNullable() | |
| 177 | + t.boolean('pass').notNullable() | |
| 178 | + t.string('reason', 1000).notNullable() | |
| 179 | + t.primary(['review_id', 'item_key']) | |
| 180 | + }) | |
| 181 | + | |
| 182 | + await db.schema.createTable('vk_disputes', (t) => { | |
| 183 | + t.string('id', 64).primary() | |
| 184 | + t.string('task_id', 64).notNullable() | |
| 185 | + t.string('opened_by_agent_id', 64).notNullable() | |
| 186 | + t.string('status', 16).notNullable() | |
| 187 | + t.json('ai_proposal').nullable() | |
| 188 | + t.json('resolution').nullable() | |
| 189 | + t.string('resolved_by_user_id', 64).nullable() | |
| 190 | + t.datetime('resolved_at', { precision: 3 }).nullable() | |
| 191 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 192 | + t.specificType('open_task_id', "varchar(64) GENERATED ALWAYS AS (CASE WHEN status <> 'resolved' THEN task_id ELSE NULL END) STORED") | |
| 193 | + t.unique(['open_task_id'], { indexName: 'uq_vk_disputes_open_task' }) | |
| 194 | + t.index(['task_id'], 'idx_vk_disputes_task') | |
| 195 | + t.index(['status', 'created_at'], 'idx_vk_disputes_queue') | |
| 196 | + t.check("status in ('open','ai_proposed','resolved')", [], 'ck_vk_disputes_status') | |
| 197 | + t.check("status <> 'ai_proposed' or ai_proposal is not null", [], 'ck_vk_disputes_ai_proposal') | |
| 198 | + t.check("(status <> 'resolved' and resolution is null and resolved_by_user_id is null and resolved_at is null) or (status = 'resolved' and resolution is not null and resolved_by_user_id is not null and resolved_at is not null)", [], 'ck_vk_disputes_resolution') | |
| 199 | + }) | |
| 200 | + | |
| 201 | + await db.schema.createTable('vk_transfers', (t) => { | |
| 202 | + t.string('id', 64).primary() | |
| 203 | + t.string('task_id', 64).notNullable() | |
| 204 | + t.string('from_agent_id', 64).notNullable() | |
| 205 | + t.string('to_agent_id', 64).notNullable() | |
| 206 | + t.string('status', 16).notNullable() | |
| 207 | + t.datetime('expires_at', { precision: 3 }).notNullable() | |
| 208 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 209 | + t.datetime('resolved_at', { precision: 3 }).nullable() | |
| 210 | + t.specificType('pending_task_id', "varchar(64) GENERATED ALWAYS AS (CASE WHEN status = 'pending' THEN task_id ELSE NULL END) STORED") | |
| 211 | + t.unique(['pending_task_id'], { indexName: 'uq_vk_transfers_pending_task' }) | |
| 212 | + t.index(['task_id'], 'idx_vk_transfers_task') | |
| 213 | + t.index(['to_agent_id', 'status'], 'idx_vk_transfers_to_agent') | |
| 214 | + t.check("status in ('pending','accepted','declined','expired')", [], 'ck_vk_transfers_status') | |
| 215 | + t.check("(status = 'pending') = (resolved_at is null)", [], 'ck_vk_transfers_resolved') | |
| 216 | + t.check('from_agent_id <> to_agent_id', [], 'ck_vk_transfers_parties') | |
| 217 | + }) | |
| 218 | + | |
| 219 | + await db.schema.createTable('vk_help_requests', (t) => { | |
| 220 | + t.string('id', 64).primary() | |
| 221 | + t.string('task_id', 64).notNullable() | |
| 222 | + t.string('agent_id', 64).notNullable() | |
| 223 | + t.string('owner_user_id', 64).notNullable() | |
| 224 | + t.text('question').notNullable() | |
| 225 | + t.string('status', 16).notNullable() | |
| 226 | + t.text('answer').nullable() | |
| 227 | + t.datetime('answered_at', { precision: 3 }).nullable() | |
| 228 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 229 | + t.index(['owner_user_id', 'status', 'created_at'], 'idx_vk_help_requests_inbox') | |
| 230 | + t.index(['task_id'], 'idx_vk_help_requests_task') | |
| 231 | + t.index(['agent_id', 'status'], 'idx_vk_help_requests_agent') | |
| 232 | + t.check("status in ('open','answered')", [], 'ck_vk_help_requests_status') | |
| 233 | + t.check("(status = 'open' and answer is null and answered_at is null) or (status = 'answered' and answer is not null and answered_at is not null)", [], 'ck_vk_help_requests_answer') | |
| 234 | + }) | |
| 235 | + | |
| 236 | + await db.schema.createTable('vk_comments', (t) => { | |
| 237 | + t.string('id', 64).primary() | |
| 238 | + t.string('task_id', 64).notNullable() | |
| 239 | + t.string('author_agent_id', 64).notNullable() | |
| 240 | + t.text('body').notNullable() | |
| 241 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 242 | + t.index(['task_id', 'created_at', 'id'], 'idx_vk_comments_task') | |
| 243 | + }) | |
| 244 | + | |
| 245 | + await db.schema.createTable('vk_events', (t) => { | |
| 246 | + t.bigIncrements('id').unsigned().primary() | |
| 247 | + t.string('guild_id', 64).notNullable() | |
| 248 | + t.string('target_agent_id', 64).nullable() | |
| 249 | + t.string('type', 64).notNullable() | |
| 250 | + t.string('task_id', 64).nullable() | |
| 251 | + t.json('payload').notNullable() | |
| 252 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 253 | + t.index(['target_agent_id', 'id'], 'idx_vk_events_target') | |
| 254 | + t.index(['guild_id', 'id'], 'idx_vk_events_guild') | |
| 255 | + t.index(['task_id', 'id'], 'idx_vk_events_task') | |
| 256 | + }) | |
| 257 | + | |
| 258 | + await db.schema.createTable('vk_jobs', (t) => { | |
| 259 | + t.string('id', 64).primary() | |
| 260 | + t.string('kind', 32).notNullable() | |
| 261 | + t.string('ref_id', 64).nullable() | |
| 262 | + t.datetime('run_at', { precision: 3 }).notNullable() | |
| 263 | + t.string('status', 16).notNullable() | |
| 264 | + t.datetime('lease_until', { precision: 3 }).nullable() | |
| 265 | + t.tinyint('attempts').unsigned().notNullable() | |
| 266 | + t.string('dedupe_key', 191).notNullable() | |
| 267 | + t.text('last_error').nullable() | |
| 268 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 269 | + t.datetime('updated_at', { precision: 3 }).nullable() | |
| 270 | + t.unique(['dedupe_key'], { indexName: 'uq_vk_jobs_dedupe' }) | |
| 271 | + t.index(['status', 'run_at'], 'idx_vk_jobs_due') | |
| 272 | + t.check("status in ('pending','running','done','failed')", [], 'ck_vk_jobs_status') | |
| 273 | + t.check("status <> 'running' or lease_until is not null", [], 'ck_vk_jobs_lease') | |
| 274 | + }) | |
| 275 | + | |
| 276 | + await db.schema.createTable('vk_escrows', (t) => { | |
| 277 | + t.string('id', 64).primary() | |
| 278 | + t.string('task_id', 64).notNullable() | |
| 279 | + t.string('payer_wallet', 64).notNullable() | |
| 280 | + t.bigInteger('amount').notNullable() | |
| 281 | + t.string('status', 16).notNullable() | |
| 282 | + t.string('deduct_ledger_id', 64).notNullable() | |
| 283 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 284 | + t.datetime('closed_at', { precision: 3 }).nullable() | |
| 285 | + t.unique(['task_id'], { indexName: 'uq_vk_escrows_task' }) | |
| 286 | + t.unique(['deduct_ledger_id'], { indexName: 'uq_vk_escrows_deduct_ledger' }) | |
| 287 | + t.index(['payer_wallet', 'status'], 'idx_vk_escrows_payer_status') | |
| 288 | + t.check("status in ('held','released','refunded')", [], 'ck_vk_escrows_status') | |
| 289 | + t.check('amount > 0', [], 'ck_vk_escrows_amount') | |
| 290 | + t.check("(status = 'held') = (closed_at is null)", [], 'ck_vk_escrows_closed') | |
| 291 | + }) | |
| 292 | + | |
| 293 | + await db.schema.createTable('vk_escrow_payouts', (t) => { | |
| 294 | + t.string('id', 64).primary() | |
| 295 | + t.string('escrow_id', 64).notNullable() | |
| 296 | + t.string('task_id', 64).notNullable() | |
| 297 | + t.string('recipient_wallet', 64).notNullable() | |
| 298 | + t.bigInteger('amount').notNullable() | |
| 299 | + t.string('kind', 16).notNullable() | |
| 300 | + t.string('ledger_id', 64).notNullable() | |
| 301 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 302 | + t.unique(['escrow_id', 'recipient_wallet', 'kind'], { indexName: 'uq_vk_escrow_payouts_recipient_kind' }) | |
| 303 | + t.unique(['ledger_id'], { indexName: 'uq_vk_escrow_payouts_ledger' }) | |
| 304 | + t.index(['task_id'], 'idx_vk_escrow_payouts_task') | |
| 305 | + t.check("kind in ('winner','relay_share','refund')", [], 'ck_vk_escrow_payouts_kind') | |
| 306 | + t.check('amount > 0', [], 'ck_vk_escrow_payouts_amount') | |
| 307 | + }) | |
| 308 | + | |
| 309 | + await db.schema.createTable('vk_agent_claims', (t) => { | |
| 310 | + t.string('id', 64).primary() | |
| 311 | + t.string('agent_id', 64).notNullable() | |
| 312 | + t.string('claim_token_hash', 64).notNullable() | |
| 313 | + t.string('verification_code', 16).notNullable() | |
| 314 | + t.datetime('expires_at', { precision: 3 }).notNullable() | |
| 315 | + t.string('claimed_by_user_id', 64).nullable() | |
| 316 | + t.datetime('claimed_at', { precision: 3 }).nullable() | |
| 317 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 318 | + t.unique(['claim_token_hash'], { indexName: 'uq_vk_agent_claims_token' }) | |
| 319 | + t.unique(['agent_id'], { indexName: 'uq_vk_agent_claims_agent' }) | |
| 320 | + t.check('(claimed_by_user_id is null) = (claimed_at is null)', [], 'ck_vk_agent_claims_claimed_pair') | |
| 321 | + }) | |
| 322 | + | |
| 323 | + await db.schema.createTable('vk_api_keys', (t) => { | |
| 324 | + t.string('id', 64).primary() | |
| 325 | + t.string('agent_id', 64).notNullable() | |
| 326 | + t.string('key_hash', 64).notNullable() | |
| 327 | + t.string('key_prefix', 16).notNullable() | |
| 328 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 329 | + t.datetime('revoked_at', { precision: 3 }).nullable() | |
| 330 | + t.specificType('active_agent_id', 'varchar(64) GENERATED ALWAYS AS (CASE WHEN revoked_at IS NULL THEN agent_id ELSE NULL END) STORED') | |
| 331 | + t.unique(['key_hash'], { indexName: 'uq_vk_api_keys_hash' }) | |
| 332 | + t.unique(['active_agent_id'], { indexName: 'uq_vk_api_keys_active_agent' }) | |
| 333 | + t.index(['agent_id'], 'idx_vk_api_keys_agent') | |
| 334 | + }) | |
| 335 | + | |
| 336 | + await db.schema.createTable('vk_webhook_deliveries', (t) => { | |
| 337 | + t.string('id', 64).primary() | |
| 338 | + t.bigInteger('event_id').unsigned().notNullable() | |
| 339 | + t.string('agent_id', 64).notNullable() | |
| 340 | + t.string('status', 16).notNullable() | |
| 341 | + t.tinyint('attempts').unsigned().notNullable() | |
| 342 | + t.datetime('next_attempt_at', { precision: 3 }).nullable() | |
| 343 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 344 | + t.datetime('delivered_at', { precision: 3 }).nullable() | |
| 345 | + t.unique(['event_id', 'agent_id'], { indexName: 'uq_vk_webhook_deliveries_event_agent' }) | |
| 346 | + t.index(['status', 'next_attempt_at'], 'idx_vk_webhook_deliveries_due') | |
| 347 | + t.check("status in ('pending','delivered','failed')", [], 'ck_vk_webhook_deliveries_status') | |
| 348 | + t.check("(status = 'pending') = (next_attempt_at is not null)", [], 'ck_vk_webhook_deliveries_next_attempt') | |
| 349 | + t.check("(status = 'delivered') = (delivered_at is not null)", [], 'ck_vk_webhook_deliveries_delivered') | |
| 350 | + }) | |
| 351 | +} | |
| 352 | + | |
| 353 | +export async function down(db) { | |
| 354 | + await db.schema.dropTable('vk_webhook_deliveries') | |
| 355 | + await db.schema.dropTable('vk_api_keys') | |
| 356 | + await db.schema.dropTable('vk_agent_claims') | |
| 357 | + await db.schema.dropTable('vk_escrow_payouts') | |
| 358 | + await db.schema.dropTable('vk_escrows') | |
| 359 | + await db.schema.dropTable('vk_jobs') | |
| 360 | + await db.schema.dropTable('vk_events') | |
| 361 | + await db.schema.dropTable('vk_comments') | |
| 362 | + await db.schema.dropTable('vk_help_requests') | |
| 363 | + await db.schema.dropTable('vk_transfers') | |
| 364 | + await db.schema.dropTable('vk_disputes') | |
| 365 | + await db.schema.dropTable('vk_review_checks') | |
| 366 | + await db.schema.dropTable('vk_reviews') | |
| 367 | + await db.schema.dropTable('vk_submissions') | |
| 368 | + await db.schema.dropTable('vk_checkpoints') | |
| 369 | + await db.schema.dropTable('vk_task_entries') | |
| 370 | + await db.schema.dropTable('vk_task_holds') | |
| 371 | + await db.schema.dropTable('vk_task_acceptance_items') | |
| 372 | + await db.schema.dropTable('vk_tasks') | |
| 373 | + await db.schema.dropTable('vk_agents') | |
| 374 | + await db.schema.dropTable('vk_guild_members') | |
| 375 | + await db.schema.dropTable('vk_guilds') | |
| 376 | +} | ... | ... |
apps/voko-server/src/db/migrations/202610100002_access_request_agent_count.js
0 → 100644
| 1 | +/** 一人最多认领 3 个 Agent:计数列 + CHECK,认领事务里自增,超出由 DB 拒绝。 */ | |
| 2 | +const TABLE = 'vk_access_requests' | |
| 3 | + | |
| 4 | +export async function up(db) { | |
| 5 | + await db.schema.alterTable(TABLE, (t) => { | |
| 6 | + t.tinyint('claimed_agent_count').unsigned().notNullable().defaultTo(0) | |
| 7 | + t.check("claimed_agent_count <= 3 and (status = 'approved' or claimed_agent_count = 0)", [], 'ck_vk_access_requests_agent_count') | |
| 8 | + }) | |
| 9 | +} | |
| 10 | + | |
| 11 | +export async function down(db) { | |
| 12 | + await db.raw('ALTER TABLE ?? DROP CHECK ??', [TABLE, 'ck_vk_access_requests_agent_count']) | |
| 13 | + await db.schema.alterTable(TABLE, (t) => t.dropColumn('claimed_agent_count')) | |
| 14 | +} | ... | ... |
apps/voko-server/src/db/schema.js
| ... | ... | @@ -3,6 +3,8 @@ import { billingDbSchema } from '@drama/billing' |
| 3 | 3 | import { dbSchema } from '@drama/db' |
| 4 | 4 | import { emailDbSchema } from '@drama/email' |
| 5 | 5 | import { accessDbSchema } from '@voko/access' |
| 6 | +import { clearingDbSchema } from '@voko/clearing' | |
| 7 | +import { exchangeDbSchema } from '@voko/exchange' | |
| 6 | 8 | |
| 7 | 9 | /** voko 库的当前完整结构(新空库 init 时按此建表)。 */ |
| 8 | 10 | export const vokoTargetTables = [ |
| ... | ... | @@ -11,4 +13,6 @@ export const vokoTargetTables = [ |
| 11 | 13 | ...billingDbSchema.targetTables, |
| 12 | 14 | ...emailDbSchema.targetTables, |
| 13 | 15 | ...accessDbSchema.targetTables, |
| 16 | + ...exchangeDbSchema.targetTables, | |
| 17 | + ...clearingDbSchema.targetTables, | |
| 14 | 18 | ] | ... | ... |
packages/voko-access/src/access-requests.js
| ... | ... | @@ -15,6 +15,7 @@ export const ACCESS_REQUEST_STATUSES = Object.freeze(['pending', 'approved', 're |
| 15 | 15 | * created_at: Date, |
| 16 | 16 | * reviewed_by_user_id: string|null, |
| 17 | 17 | * reviewed_at: Date|null, |
| 18 | + * claimed_agent_count: number, | |
| 18 | 19 | * }} AccessRequest |
| 19 | 20 | */ |
| 20 | 21 | |
| ... | ... | @@ -41,6 +42,7 @@ export function accessRequestFromRow(row) { |
| 41 | 42 | throw new Error(`Unhandled status: ${status}, id: ${id}`) |
| 42 | 43 | } |
| 43 | 44 | if (!(row.created_at instanceof Date)) throw new Error(`vk_access_requests.created_at missing, id: ${id}`) |
| 45 | + if (!Number.isInteger(row.claimed_agent_count)) throw new Error(`vk_access_requests.claimed_agent_count invalid, id: ${id}`) | |
| 44 | 46 | return { |
| 45 | 47 | id, |
| 46 | 48 | user_id: requiredString(row.user_id, 'user_id', id), |
| ... | ... | @@ -50,6 +52,7 @@ export function accessRequestFromRow(row) { |
| 50 | 52 | created_at: row.created_at, |
| 51 | 53 | reviewed_by_user_id: row.reviewed_by_user_id, |
| 52 | 54 | reviewed_at: row.reviewed_at, |
| 55 | + claimed_agent_count: row.claimed_agent_count, | |
| 53 | 56 | } |
| 54 | 57 | } |
| 55 | 58 | |
| ... | ... | @@ -71,7 +74,7 @@ export function createAccessRequests({ db, notifier }) { |
| 71 | 74 | */ |
| 72 | 75 | async function submit(ctx, { user_id, email, note }) { |
| 73 | 76 | await db(TABLE) |
| 74 | - .insert({ id: `ar_${randomUUID()}`, user_id, email, note, status: 'pending', created_at: new Date(), reviewed_by_user_id: null, reviewed_at: null }) | |
| 77 | + .insert({ id: `ar_${randomUUID()}`, user_id, email, note, status: 'pending', created_at: new Date(), reviewed_by_user_id: null, reviewed_at: null, claimed_agent_count: 0 }) | |
| 75 | 78 | .onConflict('user_id') |
| 76 | 79 | .ignore() |
| 77 | 80 | return getForUser(ctx, { user_id }) | ... | ... |
packages/voko-access/src/db/tables/index.cjs
packages/voko-access/src/db/tables/vk_access_requests.cjs
| ... | ... | @@ -9,10 +9,12 @@ function buildTable(t) { |
| 9 | 9 | t.datetime('created_at', { precision: 3 }).notNullable() |
| 10 | 10 | t.string('reviewed_by_user_id', 64).nullable() |
| 11 | 11 | t.datetime('reviewed_at', { precision: 3 }).nullable() |
| 12 | + t.tinyint('claimed_agent_count').unsigned().notNullable().defaultTo(0) | |
| 12 | 13 | t.unique(['user_id'], { indexName: 'uq_vk_access_requests_user' }) |
| 13 | 14 | t.index(['status', 'created_at', 'id'], 'idx_vk_access_requests_queue') |
| 14 | 15 | t.check("status in ('pending','approved','rejected')", [], 'ck_vk_access_requests_status') |
| 15 | 16 | t.check("(status = 'pending' and reviewed_at is null and reviewed_by_user_id is null) or (status <> 'pending' and reviewed_at is not null and reviewed_by_user_id is not null)", [], 'ck_vk_access_requests_review') |
| 17 | + t.check("claimed_agent_count <= 3 and (status = 'approved' or claimed_agent_count = 0)", [], 'ck_vk_access_requests_agent_count') | |
| 16 | 18 | } |
| 17 | 19 | |
| 18 | 20 | module.exports = { tableName, buildTable } | ... | ... |
packages/voko-access/src/db/tables/vk_agent_claims.cjs
0 → 100644
| 1 | +const tableName = 'vk_agent_claims' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('agent_id', 64).notNullable() | |
| 6 | + t.string('claim_token_hash', 64).notNullable() | |
| 7 | + t.string('verification_code', 16).notNullable() | |
| 8 | + t.datetime('expires_at', { precision: 3 }).notNullable() | |
| 9 | + t.string('claimed_by_user_id', 64).nullable() | |
| 10 | + t.datetime('claimed_at', { precision: 3 }).nullable() | |
| 11 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 12 | + t.unique(['claim_token_hash'], { indexName: 'uq_vk_agent_claims_token' }) | |
| 13 | + t.unique(['agent_id'], { indexName: 'uq_vk_agent_claims_agent' }) | |
| 14 | + t.check('(claimed_by_user_id is null) = (claimed_at is null)', [], 'ck_vk_agent_claims_claimed_pair') | |
| 15 | +} | |
| 16 | + | |
| 17 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-access/src/db/tables/vk_api_keys.cjs
0 → 100644
| 1 | +const tableName = 'vk_api_keys' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('agent_id', 64).notNullable() | |
| 6 | + t.string('key_hash', 64).notNullable() | |
| 7 | + t.string('key_prefix', 16).notNullable() | |
| 8 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 9 | + t.datetime('revoked_at', { precision: 3 }).nullable() | |
| 10 | + t.specificType('active_agent_id', 'varchar(64) GENERATED ALWAYS AS (CASE WHEN revoked_at IS NULL THEN agent_id ELSE NULL END) STORED') | |
| 11 | + t.unique(['key_hash'], { indexName: 'uq_vk_api_keys_hash' }) | |
| 12 | + t.unique(['active_agent_id'], { indexName: 'uq_vk_api_keys_active_agent' }) | |
| 13 | + t.index(['agent_id'], 'idx_vk_api_keys_agent') | |
| 14 | +} | |
| 15 | + | |
| 16 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-access/src/db/tables/vk_webhook_deliveries.cjs
0 → 100644
| 1 | +const tableName = 'vk_webhook_deliveries' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.bigInteger('event_id').unsigned().notNullable() | |
| 6 | + t.string('agent_id', 64).notNullable() | |
| 7 | + t.string('status', 16).notNullable() | |
| 8 | + t.tinyint('attempts').unsigned().notNullable() | |
| 9 | + t.datetime('next_attempt_at', { precision: 3 }).nullable() | |
| 10 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 11 | + t.datetime('delivered_at', { precision: 3 }).nullable() | |
| 12 | + t.unique(['event_id', 'agent_id'], { indexName: 'uq_vk_webhook_deliveries_event_agent' }) | |
| 13 | + t.index(['status', 'next_attempt_at'], 'idx_vk_webhook_deliveries_due') | |
| 14 | + t.check("status in ('pending','delivered','failed')", [], 'ck_vk_webhook_deliveries_status') | |
| 15 | + t.check("(status = 'pending') = (next_attempt_at is not null)", [], 'ck_vk_webhook_deliveries_next_attempt') | |
| 16 | + t.check("(status = 'delivered') = (delivered_at is not null)", [], 'ck_vk_webhook_deliveries_delivered') | |
| 17 | +} | |
| 18 | + | |
| 19 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-access/test/access-request-from-row.test.js
packages/voko-clearing/package.json
0 → 100644
packages/voko-clearing/src/db/tables/index.cjs
0 → 100644
packages/voko-clearing/src/db/tables/vk_escrow_payouts.cjs
0 → 100644
| 1 | +const tableName = 'vk_escrow_payouts' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('escrow_id', 64).notNullable() | |
| 6 | + t.string('task_id', 64).notNullable() | |
| 7 | + t.string('recipient_wallet', 64).notNullable() | |
| 8 | + t.bigInteger('amount').notNullable() | |
| 9 | + t.string('kind', 16).notNullable() | |
| 10 | + t.string('ledger_id', 64).notNullable() | |
| 11 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 12 | + t.unique(['escrow_id', 'recipient_wallet', 'kind'], { indexName: 'uq_vk_escrow_payouts_recipient_kind' }) | |
| 13 | + t.unique(['ledger_id'], { indexName: 'uq_vk_escrow_payouts_ledger' }) | |
| 14 | + t.index(['task_id'], 'idx_vk_escrow_payouts_task') | |
| 15 | + t.check("kind in ('winner','relay_share','refund')", [], 'ck_vk_escrow_payouts_kind') | |
| 16 | + t.check('amount > 0', [], 'ck_vk_escrow_payouts_amount') | |
| 17 | +} | |
| 18 | + | |
| 19 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-clearing/src/db/tables/vk_escrows.cjs
0 → 100644
| 1 | +const tableName = 'vk_escrows' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('payer_wallet', 64).notNullable() | |
| 7 | + t.bigInteger('amount').notNullable() | |
| 8 | + t.string('status', 16).notNullable() | |
| 9 | + t.string('deduct_ledger_id', 64).notNullable() | |
| 10 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 11 | + t.datetime('closed_at', { precision: 3 }).nullable() | |
| 12 | + t.unique(['task_id'], { indexName: 'uq_vk_escrows_task' }) | |
| 13 | + t.unique(['deduct_ledger_id'], { indexName: 'uq_vk_escrows_deduct_ledger' }) | |
| 14 | + t.index(['payer_wallet', 'status'], 'idx_vk_escrows_payer_status') | |
| 15 | + t.check("status in ('held','released','refunded')", [], 'ck_vk_escrows_status') | |
| 16 | + t.check('amount > 0', [], 'ck_vk_escrows_amount') | |
| 17 | + t.check("(status = 'held') = (closed_at is null)", [], 'ck_vk_escrows_closed') | |
| 18 | +} | |
| 19 | + | |
| 20 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-clearing/src/index.js
0 → 100644
packages/voko-exchange/package.json
0 → 100644
packages/voko-exchange/src/db/tables/index.cjs
0 → 100644
| 1 | +const tableModules = [ | |
| 2 | + require('./vk_guilds.cjs'), | |
| 3 | + require('./vk_guild_members.cjs'), | |
| 4 | + require('./vk_agents.cjs'), | |
| 5 | + require('./vk_tasks.cjs'), | |
| 6 | + require('./vk_task_acceptance_items.cjs'), | |
| 7 | + require('./vk_task_holds.cjs'), | |
| 8 | + require('./vk_task_entries.cjs'), | |
| 9 | + require('./vk_checkpoints.cjs'), | |
| 10 | + require('./vk_submissions.cjs'), | |
| 11 | + require('./vk_reviews.cjs'), | |
| 12 | + require('./vk_review_checks.cjs'), | |
| 13 | + require('./vk_disputes.cjs'), | |
| 14 | + require('./vk_transfers.cjs'), | |
| 15 | + require('./vk_help_requests.cjs'), | |
| 16 | + require('./vk_comments.cjs'), | |
| 17 | + require('./vk_events.cjs'), | |
| 18 | + require('./vk_jobs.cjs'), | |
| 19 | +] | |
| 20 | + | |
| 21 | +const targetTables = tableModules.map(({ tableName, buildTable }) => [tableName, buildTable]) | |
| 22 | + | |
| 23 | +module.exports = { targetTables } | ... | ... |
packages/voko-exchange/src/db/tables/vk_agents.cjs
0 → 100644
| 1 | +const tableName = 'vk_agents' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('wallet_user_id', 64).notNullable() | |
| 6 | + t.string('owner_user_id', 64).nullable() | |
| 7 | + t.string('guild_id', 64).nullable() | |
| 8 | + t.string('role', 16).notNullable() | |
| 9 | + t.string('status', 16).notNullable() | |
| 10 | + t.string('name', 120).notNullable() | |
| 11 | + t.string('description', 1000).nullable() | |
| 12 | + t.json('skills').notNullable() | |
| 13 | + t.string('owner_email', 320).nullable() | |
| 14 | + t.integer('level').unsigned().notNullable() | |
| 15 | + t.bigInteger('xp').unsigned().notNullable() | |
| 16 | + t.string('webhook_url', 2048).nullable() | |
| 17 | + t.text('webhook_secret_enc').nullable() | |
| 18 | + t.bigInteger('daily_spend_limit').nullable() | |
| 19 | + t.bigInteger('events_cursor').unsigned().notNullable() | |
| 20 | + t.datetime('claimed_at', { precision: 3 }).nullable() | |
| 21 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 22 | + t.datetime('updated_at', { precision: 3 }).nullable() | |
| 23 | + t.unique(['wallet_user_id'], { indexName: 'uq_vk_agents_wallet' }) | |
| 24 | + t.index(['owner_user_id'], 'idx_vk_agents_owner') | |
| 25 | + t.index(['guild_id', 'status'], 'idx_vk_agents_guild_status') | |
| 26 | + t.check("role in ('company','member')", [], 'ck_vk_agents_role') | |
| 27 | + t.check("status in ('pending_claim','active','suspended')", [], 'ck_vk_agents_status') | |
| 28 | + t.check("(status = 'pending_claim' and owner_user_id is null and claimed_at is null and guild_id is null) or (status <> 'pending_claim' and owner_user_id is not null and claimed_at is not null and guild_id is not null)", [], 'ck_vk_agents_claim') | |
| 29 | + t.check('(webhook_url is null) = (webhook_secret_enc is null)', [], 'ck_vk_agents_webhook_pair') | |
| 30 | + t.check('level >= 1', [], 'ck_vk_agents_level') | |
| 31 | + t.check('daily_spend_limit is null or daily_spend_limit >= 0', [], 'ck_vk_agents_daily_spend_limit') | |
| 32 | +} | |
| 33 | + | |
| 34 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_checkpoints.cjs
0 → 100644
| 1 | +const tableName = 'vk_checkpoints' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('hold_id', 64).notNullable() | |
| 7 | + t.string('agent_id', 64).notNullable() | |
| 8 | + t.tinyint('progress').unsigned().notNullable() | |
| 9 | + t.string('note', 2000).nullable() | |
| 10 | + t.json('links').notNullable() | |
| 11 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 12 | + t.index(['task_id', 'created_at'], 'idx_vk_checkpoints_task') | |
| 13 | + t.index(['hold_id'], 'idx_vk_checkpoints_hold') | |
| 14 | + t.check('progress <= 100', [], 'ck_vk_checkpoints_progress') | |
| 15 | +} | |
| 16 | + | |
| 17 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_comments.cjs
0 → 100644
| 1 | +const tableName = 'vk_comments' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('author_agent_id', 64).notNullable() | |
| 7 | + t.text('body').notNullable() | |
| 8 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 9 | + t.index(['task_id', 'created_at', 'id'], 'idx_vk_comments_task') | |
| 10 | +} | |
| 11 | + | |
| 12 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_disputes.cjs
0 → 100644
| 1 | +const tableName = 'vk_disputes' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('opened_by_agent_id', 64).notNullable() | |
| 7 | + t.string('status', 16).notNullable() | |
| 8 | + t.json('ai_proposal').nullable() | |
| 9 | + t.json('resolution').nullable() | |
| 10 | + t.string('resolved_by_user_id', 64).nullable() | |
| 11 | + t.datetime('resolved_at', { precision: 3 }).nullable() | |
| 12 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 13 | + t.specificType('open_task_id', "varchar(64) GENERATED ALWAYS AS (CASE WHEN status <> 'resolved' THEN task_id ELSE NULL END) STORED") | |
| 14 | + t.unique(['open_task_id'], { indexName: 'uq_vk_disputes_open_task' }) | |
| 15 | + t.index(['task_id'], 'idx_vk_disputes_task') | |
| 16 | + t.index(['status', 'created_at'], 'idx_vk_disputes_queue') | |
| 17 | + t.check("status in ('open','ai_proposed','resolved')", [], 'ck_vk_disputes_status') | |
| 18 | + t.check("status <> 'ai_proposed' or ai_proposal is not null", [], 'ck_vk_disputes_ai_proposal') | |
| 19 | + t.check("(status <> 'resolved' and resolution is null and resolved_by_user_id is null and resolved_at is null) or (status = 'resolved' and resolution is not null and resolved_by_user_id is not null and resolved_at is not null)", [], 'ck_vk_disputes_resolution') | |
| 20 | +} | |
| 21 | + | |
| 22 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_events.cjs
0 → 100644
| 1 | +const tableName = 'vk_events' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.bigIncrements('id').unsigned().primary() | |
| 5 | + t.string('guild_id', 64).notNullable() | |
| 6 | + t.string('target_agent_id', 64).nullable() | |
| 7 | + t.string('type', 64).notNullable() | |
| 8 | + t.string('task_id', 64).nullable() | |
| 9 | + t.json('payload').notNullable() | |
| 10 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 11 | + t.index(['target_agent_id', 'id'], 'idx_vk_events_target') | |
| 12 | + t.index(['guild_id', 'id'], 'idx_vk_events_guild') | |
| 13 | + t.index(['task_id', 'id'], 'idx_vk_events_task') | |
| 14 | +} | |
| 15 | + | |
| 16 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_guild_members.cjs
0 → 100644
| 1 | +const tableName = 'vk_guild_members' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('guild_id', 64).notNullable() | |
| 6 | + t.string('user_id', 64).notNullable() | |
| 7 | + t.string('role', 16).notNullable() | |
| 8 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 9 | + t.unique(['guild_id', 'user_id'], { indexName: 'uq_vk_guild_members_guild_user' }) | |
| 10 | + t.index(['user_id'], 'idx_vk_guild_members_user') | |
| 11 | + t.check("role in ('lead','member')", [], 'ck_vk_guild_members_role') | |
| 12 | +} | |
| 13 | + | |
| 14 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_guilds.cjs
0 → 100644
| 1 | +const tableName = 'vk_guilds' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('name', 120).notNullable() | |
| 6 | + t.string('slug', 64).notNullable() | |
| 7 | + t.string('lead_user_id', 64).notNullable() | |
| 8 | + t.string('company_agent_id', 64).nullable() | |
| 9 | + t.boolean('require_lead_approval').notNullable() | |
| 10 | + t.integer('checkpoint_interval_sec').unsigned().notNullable().defaultTo(21600) | |
| 11 | + t.integer('piece_review_window_sec').unsigned().notNullable().defaultTo(86400) | |
| 12 | + t.integer('contest_review_window_sec').unsigned().notNullable().defaultTo(259200) | |
| 13 | + t.integer('transfer_ttl_sec').unsigned().notNullable().defaultTo(86400) | |
| 14 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 15 | + t.datetime('updated_at', { precision: 3 }).nullable() | |
| 16 | + t.unique(['slug'], { indexName: 'uq_vk_guilds_slug' }) | |
| 17 | + t.check('checkpoint_interval_sec > 0 and piece_review_window_sec > 0 and contest_review_window_sec > 0 and transfer_ttl_sec > 0', [], 'ck_vk_guilds_windows') | |
| 18 | +} | |
| 19 | + | |
| 20 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_help_requests.cjs
0 → 100644
| 1 | +const tableName = 'vk_help_requests' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('agent_id', 64).notNullable() | |
| 7 | + t.string('owner_user_id', 64).notNullable() | |
| 8 | + t.text('question').notNullable() | |
| 9 | + t.string('status', 16).notNullable() | |
| 10 | + t.text('answer').nullable() | |
| 11 | + t.datetime('answered_at', { precision: 3 }).nullable() | |
| 12 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 13 | + t.index(['owner_user_id', 'status', 'created_at'], 'idx_vk_help_requests_inbox') | |
| 14 | + t.index(['task_id'], 'idx_vk_help_requests_task') | |
| 15 | + t.index(['agent_id', 'status'], 'idx_vk_help_requests_agent') | |
| 16 | + t.check("status in ('open','answered')", [], 'ck_vk_help_requests_status') | |
| 17 | + t.check("(status = 'open' and answer is null and answered_at is null) or (status = 'answered' and answer is not null and answered_at is not null)", [], 'ck_vk_help_requests_answer') | |
| 18 | +} | |
| 19 | + | |
| 20 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_jobs.cjs
0 → 100644
| 1 | +const tableName = 'vk_jobs' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('kind', 32).notNullable() | |
| 6 | + t.string('ref_id', 64).nullable() | |
| 7 | + t.datetime('run_at', { precision: 3 }).notNullable() | |
| 8 | + t.string('status', 16).notNullable() | |
| 9 | + t.datetime('lease_until', { precision: 3 }).nullable() | |
| 10 | + t.tinyint('attempts').unsigned().notNullable() | |
| 11 | + t.string('dedupe_key', 191).notNullable() | |
| 12 | + t.text('last_error').nullable() | |
| 13 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 14 | + t.datetime('updated_at', { precision: 3 }).nullable() | |
| 15 | + t.unique(['dedupe_key'], { indexName: 'uq_vk_jobs_dedupe' }) | |
| 16 | + t.index(['status', 'run_at'], 'idx_vk_jobs_due') | |
| 17 | + t.check("status in ('pending','running','done','failed')", [], 'ck_vk_jobs_status') | |
| 18 | + t.check("status <> 'running' or lease_until is not null", [], 'ck_vk_jobs_lease') | |
| 19 | +} | |
| 20 | + | |
| 21 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_review_checks.cjs
0 → 100644
| 1 | +const tableName = 'vk_review_checks' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('review_id', 64).notNullable() | |
| 5 | + t.string('item_key', 32).notNullable() | |
| 6 | + t.boolean('pass').notNullable() | |
| 7 | + t.string('reason', 1000).notNullable() | |
| 8 | + t.primary(['review_id', 'item_key']) | |
| 9 | +} | |
| 10 | + | |
| 11 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_reviews.cjs
0 → 100644
| 1 | +const tableName = 'vk_reviews' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('submission_id', 64).notNullable() | |
| 7 | + t.string('reviewer_kind', 16).notNullable() | |
| 8 | + t.string('action', 16).notNullable() | |
| 9 | + t.text('reason').nullable() | |
| 10 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 11 | + t.index(['task_id', 'created_at'], 'idx_vk_reviews_task') | |
| 12 | + t.index(['submission_id'], 'idx_vk_reviews_submission') | |
| 13 | + t.check("reviewer_kind in ('publisher','auto','arbitration')", [], 'ck_vk_reviews_reviewer_kind') | |
| 14 | + t.check("action in ('accept','reject','select_winner')", [], 'ck_vk_reviews_action') | |
| 15 | +} | |
| 16 | + | |
| 17 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_submissions.cjs
0 → 100644
| 1 | +const tableName = 'vk_submissions' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('agent_id', 64).notNullable() | |
| 7 | + t.tinyint('round').unsigned().notNullable() | |
| 8 | + t.text('summary').notNullable() | |
| 9 | + t.json('artifacts').notNullable() | |
| 10 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 11 | + t.unique(['task_id', 'agent_id', 'round'], { indexName: 'uq_vk_submissions_task_agent_round' }) | |
| 12 | + t.check('round between 1 and 3', [], 'ck_vk_submissions_round') | |
| 13 | +} | |
| 14 | + | |
| 15 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_task_acceptance_items.cjs
0 → 100644
| 1 | +const tableName = 'vk_task_acceptance_items' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('task_id', 64).notNullable() | |
| 5 | + t.string('item_key', 32).notNullable() | |
| 6 | + t.tinyint('ordinal').unsigned().notNullable() | |
| 7 | + t.string('text', 1000).notNullable() | |
| 8 | + t.primary(['task_id', 'item_key']) | |
| 9 | + t.unique(['task_id', 'ordinal'], { indexName: 'uq_vk_task_acceptance_items_ordinal' }) | |
| 10 | + t.check('ordinal between 1 and 5', [], 'ck_vk_task_acceptance_items_ordinal') | |
| 11 | +} | |
| 12 | + | |
| 13 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_task_entries.cjs
0 → 100644
| 1 | +const tableName = 'vk_task_entries' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('agent_id', 64).notNullable() | |
| 7 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 8 | + t.unique(['task_id', 'agent_id'], { indexName: 'uq_vk_task_entries_task_agent' }) | |
| 9 | + t.index(['agent_id'], 'idx_vk_task_entries_agent') | |
| 10 | +} | |
| 11 | + | |
| 12 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_task_holds.cjs
0 → 100644
| 1 | +const tableName = 'vk_task_holds' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('agent_id', 64).notNullable() | |
| 7 | + t.datetime('started_at', { precision: 3 }).notNullable() | |
| 8 | + t.datetime('ended_at', { precision: 3 }).nullable() | |
| 9 | + t.string('end_reason', 16).nullable() | |
| 10 | + t.boolean('has_valid_checkpoint').notNullable() | |
| 11 | + t.specificType('open_task_id', 'varchar(64) GENERATED ALWAYS AS (CASE WHEN ended_at IS NULL THEN task_id ELSE NULL END) STORED') | |
| 12 | + t.unique(['open_task_id'], { indexName: 'uq_vk_task_holds_open_task' }) | |
| 13 | + t.index(['task_id', 'started_at'], 'idx_vk_task_holds_task') | |
| 14 | + t.index(['agent_id', 'ended_at'], 'idx_vk_task_holds_agent') | |
| 15 | + t.check("end_reason in ('submitted','released','timeout','transferred')", [], 'ck_vk_task_holds_end_reason') | |
| 16 | + t.check('(ended_at is null) = (end_reason is null)', [], 'ck_vk_task_holds_end_pair') | |
| 17 | +} | |
| 18 | + | |
| 19 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_tasks.cjs
0 → 100644
| 1 | +const tableName = 'vk_tasks' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('guild_id', 64).notNullable() | |
| 6 | + t.string('publisher_agent_id', 64).notNullable() | |
| 7 | + t.string('template', 16).notNullable() | |
| 8 | + t.string('title', 200).notNullable() | |
| 9 | + t.text('spec').notNullable() | |
| 10 | + t.bigInteger('reward').notNullable() | |
| 11 | + t.tinyint('rarity').unsigned().notNullable() | |
| 12 | + t.datetime('deadline_at', { precision: 3 }).notNullable() | |
| 13 | + t.string('status', 24).notNullable() | |
| 14 | + t.string('client_request_key', 191).notNullable() | |
| 15 | + t.string('holder_agent_id', 64).nullable() | |
| 16 | + t.datetime('next_checkpoint_due_at', { precision: 3 }).nullable() | |
| 17 | + t.tinyint('missed_checkpoints').unsigned().notNullable() | |
| 18 | + t.tinyint('rework_count').unsigned().notNullable() | |
| 19 | + t.datetime('review_due_at', { precision: 3 }).nullable() | |
| 20 | + t.datetime('settled_at', { precision: 3 }).nullable() | |
| 21 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 22 | + t.datetime('updated_at', { precision: 3 }).nullable() | |
| 23 | + t.unique(['publisher_agent_id', 'client_request_key'], { indexName: 'uq_vk_tasks_publisher_request' }) | |
| 24 | + t.index(['guild_id', 'status', 'deadline_at'], 'idx_vk_tasks_board') | |
| 25 | + t.index(['publisher_agent_id', 'status'], 'idx_vk_tasks_publisher_status') | |
| 26 | + t.index(['holder_agent_id', 'status'], 'idx_vk_tasks_holder_status') | |
| 27 | + t.check("template in ('piecework','contest')", [], 'ck_vk_tasks_template') | |
| 28 | + t.check("status in ('pending_approval','open','claimed','in_contest','submitted','reviewing','rework','disputed','settled','cancelled','refunded')", [], 'ck_vk_tasks_status') | |
| 29 | + t.check('reward > 0', [], 'ck_vk_tasks_reward') | |
| 30 | + t.check('rarity between 1 and 10', [], 'ck_vk_tasks_rarity') | |
| 31 | + t.check('rework_count <= 2', [], 'ck_vk_tasks_rework_count') | |
| 32 | + t.check("status not in ('claimed','submitted','rework') or template = 'piecework'", [], 'ck_vk_tasks_piecework_status') | |
| 33 | + t.check("status not in ('in_contest','reviewing') or template = 'contest'", [], 'ck_vk_tasks_contest_status') | |
| 34 | + t.check("template <> 'contest' or holder_agent_id is null", [], 'ck_vk_tasks_contest_holder') | |
| 35 | + t.check("status <> 'claimed' or (holder_agent_id is not null and next_checkpoint_due_at is not null)", [], 'ck_vk_tasks_claimed') | |
| 36 | + t.check("status not in ('submitted','reviewing') or review_due_at is not null", [], 'ck_vk_tasks_review_due') | |
| 37 | + t.check("status <> 'settled' or settled_at is not null", [], 'ck_vk_tasks_settled') | |
| 38 | +} | |
| 39 | + | |
| 40 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/db/tables/vk_transfers.cjs
0 → 100644
| 1 | +const tableName = 'vk_transfers' | |
| 2 | + | |
| 3 | +function buildTable(t) { | |
| 4 | + t.string('id', 64).primary() | |
| 5 | + t.string('task_id', 64).notNullable() | |
| 6 | + t.string('from_agent_id', 64).notNullable() | |
| 7 | + t.string('to_agent_id', 64).notNullable() | |
| 8 | + t.string('status', 16).notNullable() | |
| 9 | + t.datetime('expires_at', { precision: 3 }).notNullable() | |
| 10 | + t.datetime('created_at', { precision: 3 }).notNullable() | |
| 11 | + t.datetime('resolved_at', { precision: 3 }).nullable() | |
| 12 | + t.specificType('pending_task_id', "varchar(64) GENERATED ALWAYS AS (CASE WHEN status = 'pending' THEN task_id ELSE NULL END) STORED") | |
| 13 | + t.unique(['pending_task_id'], { indexName: 'uq_vk_transfers_pending_task' }) | |
| 14 | + t.index(['task_id'], 'idx_vk_transfers_task') | |
| 15 | + t.index(['to_agent_id', 'status'], 'idx_vk_transfers_to_agent') | |
| 16 | + t.check("status in ('pending','accepted','declined','expired')", [], 'ck_vk_transfers_status') | |
| 17 | + t.check("(status = 'pending') = (resolved_at is null)", [], 'ck_vk_transfers_resolved') | |
| 18 | + t.check('from_agent_id <> to_agent_id', [], 'ck_vk_transfers_parties') | |
| 19 | +} | |
| 20 | + | |
| 21 | +module.exports = { tableName, buildTable } | ... | ... |
packages/voko-exchange/src/index.js
0 → 100644
pnpm-lock.yaml
| ... | ... | @@ -69,6 +69,12 @@ importers: |
| 69 | 69 | '@voko/access': |
| 70 | 70 | specifier: workspace:* |
| 71 | 71 | version: link:../../packages/voko-access |
| 72 | + '@voko/clearing': | |
| 73 | + specifier: workspace:* | |
| 74 | + version: link:../../packages/voko-clearing | |
| 75 | + '@voko/exchange': | |
| 76 | + specifier: workspace:* | |
| 77 | + version: link:../../packages/voko-exchange | |
| 72 | 78 | dotenv: |
| 73 | 79 | specifier: 16.6.1 |
| 74 | 80 | version: 16.6.1 |
| ... | ... | @@ -286,6 +292,10 @@ importers: |
| 286 | 292 | specifier: ^2.1.4 |
| 287 | 293 | version: 2.1.9(@types/node@24.13.2)(jsdom@25.0.1)(sass@1.101.0) |
| 288 | 294 | |
| 295 | + packages/voko-clearing: {} | |
| 296 | + | |
| 297 | + packages/voko-exchange: {} | |
| 298 | + | |
| 289 | 299 | packages: |
| 290 | 300 | |
| 291 | 301 | '@asamuzakjp/css-color@3.2.0': | ... | ... |