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,6 +23,8 @@ | ||
| 23 | "@drama/observability": "workspace:*", | 23 | "@drama/observability": "workspace:*", |
| 24 | "@drama/server-kit": "workspace:*", | 24 | "@drama/server-kit": "workspace:*", |
| 25 | "@voko/access": "workspace:*", | 25 | "@voko/access": "workspace:*", |
| 26 | + "@voko/clearing": "workspace:*", | ||
| 27 | + "@voko/exchange": "workspace:*", | ||
| 26 | "dotenv": "16.6.1", | 28 | "dotenv": "16.6.1", |
| 27 | "fastify": "^5.1.0", | 29 | "fastify": "^5.1.0", |
| 28 | "googleapis": "^173.0.0", | 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,6 +3,8 @@ import { billingDbSchema } from '@drama/billing' | ||
| 3 | import { dbSchema } from '@drama/db' | 3 | import { dbSchema } from '@drama/db' |
| 4 | import { emailDbSchema } from '@drama/email' | 4 | import { emailDbSchema } from '@drama/email' |
| 5 | import { accessDbSchema } from '@voko/access' | 5 | import { accessDbSchema } from '@voko/access' |
| 6 | +import { clearingDbSchema } from '@voko/clearing' | ||
| 7 | +import { exchangeDbSchema } from '@voko/exchange' | ||
| 6 | 8 | ||
| 7 | /** voko 库的当前完整结构(新空库 init 时按此建表)。 */ | 9 | /** voko 库的当前完整结构(新空库 init 时按此建表)。 */ |
| 8 | export const vokoTargetTables = [ | 10 | export const vokoTargetTables = [ |
| @@ -11,4 +13,6 @@ export const vokoTargetTables = [ | @@ -11,4 +13,6 @@ export const vokoTargetTables = [ | ||
| 11 | ...billingDbSchema.targetTables, | 13 | ...billingDbSchema.targetTables, |
| 12 | ...emailDbSchema.targetTables, | 14 | ...emailDbSchema.targetTables, |
| 13 | ...accessDbSchema.targetTables, | 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,6 +15,7 @@ export const ACCESS_REQUEST_STATUSES = Object.freeze(['pending', 'approved', 're | ||
| 15 | * created_at: Date, | 15 | * created_at: Date, |
| 16 | * reviewed_by_user_id: string|null, | 16 | * reviewed_by_user_id: string|null, |
| 17 | * reviewed_at: Date|null, | 17 | * reviewed_at: Date|null, |
| 18 | + * claimed_agent_count: number, | ||
| 18 | * }} AccessRequest | 19 | * }} AccessRequest |
| 19 | */ | 20 | */ |
| 20 | 21 | ||
| @@ -41,6 +42,7 @@ export function accessRequestFromRow(row) { | @@ -41,6 +42,7 @@ export function accessRequestFromRow(row) { | ||
| 41 | throw new Error(`Unhandled status: ${status}, id: ${id}`) | 42 | throw new Error(`Unhandled status: ${status}, id: ${id}`) |
| 42 | } | 43 | } |
| 43 | if (!(row.created_at instanceof Date)) throw new Error(`vk_access_requests.created_at missing, id: ${id}`) | 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 | return { | 46 | return { |
| 45 | id, | 47 | id, |
| 46 | user_id: requiredString(row.user_id, 'user_id', id), | 48 | user_id: requiredString(row.user_id, 'user_id', id), |
| @@ -50,6 +52,7 @@ export function accessRequestFromRow(row) { | @@ -50,6 +52,7 @@ export function accessRequestFromRow(row) { | ||
| 50 | created_at: row.created_at, | 52 | created_at: row.created_at, |
| 51 | reviewed_by_user_id: row.reviewed_by_user_id, | 53 | reviewed_by_user_id: row.reviewed_by_user_id, |
| 52 | reviewed_at: row.reviewed_at, | 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,7 +74,7 @@ export function createAccessRequests({ db, notifier }) { | ||
| 71 | */ | 74 | */ |
| 72 | async function submit(ctx, { user_id, email, note }) { | 75 | async function submit(ctx, { user_id, email, note }) { |
| 73 | await db(TABLE) | 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 | .onConflict('user_id') | 78 | .onConflict('user_id') |
| 76 | .ignore() | 79 | .ignore() |
| 77 | return getForUser(ctx, { user_id }) | 80 | return getForUser(ctx, { user_id }) |
packages/voko-access/src/db/tables/index.cjs
| 1 | const tableModules = [ | 1 | const tableModules = [ |
| 2 | require('./vk_access_requests.cjs'), | 2 | require('./vk_access_requests.cjs'), |
| 3 | + require('./vk_agent_claims.cjs'), | ||
| 4 | + require('./vk_api_keys.cjs'), | ||
| 5 | + require('./vk_webhook_deliveries.cjs'), | ||
| 3 | ] | 6 | ] |
| 4 | 7 | ||
| 5 | const targetTables = tableModules.map(({ tableName, buildTable }) => [tableName, buildTable]) | 8 | const targetTables = tableModules.map(({ tableName, buildTable }) => [tableName, buildTable]) |
packages/voko-access/src/db/tables/vk_access_requests.cjs
| @@ -9,10 +9,12 @@ function buildTable(t) { | @@ -9,10 +9,12 @@ function buildTable(t) { | ||
| 9 | t.datetime('created_at', { precision: 3 }).notNullable() | 9 | t.datetime('created_at', { precision: 3 }).notNullable() |
| 10 | t.string('reviewed_by_user_id', 64).nullable() | 10 | t.string('reviewed_by_user_id', 64).nullable() |
| 11 | t.datetime('reviewed_at', { precision: 3 }).nullable() | 11 | t.datetime('reviewed_at', { precision: 3 }).nullable() |
| 12 | + t.tinyint('claimed_agent_count').unsigned().notNullable().defaultTo(0) | ||
| 12 | t.unique(['user_id'], { indexName: 'uq_vk_access_requests_user' }) | 13 | t.unique(['user_id'], { indexName: 'uq_vk_access_requests_user' }) |
| 13 | t.index(['status', 'created_at', 'id'], 'idx_vk_access_requests_queue') | 14 | t.index(['status', 'created_at', 'id'], 'idx_vk_access_requests_queue') |
| 14 | t.check("status in ('pending','approved','rejected')", [], 'ck_vk_access_requests_status') | 15 | t.check("status in ('pending','approved','rejected')", [], 'ck_vk_access_requests_status') |
| 15 | 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') | 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 | module.exports = { tableName, buildTable } | 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
| @@ -7,6 +7,7 @@ const base = { | @@ -7,6 +7,7 @@ const base = { | ||
| 7 | email: 'a@example.com', | 7 | email: 'a@example.com', |
| 8 | note: null, | 8 | note: null, |
| 9 | created_at: new Date('2026-10-10T00:00:00Z'), | 9 | created_at: new Date('2026-10-10T00:00:00Z'), |
| 10 | + claimed_agent_count: 0, | ||
| 10 | } | 11 | } |
| 11 | 12 | ||
| 12 | describe('accessRequestFromRow 覆盖全部状态', () => { | 13 | describe('accessRequestFromRow 覆盖全部状态', () => { |
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,6 +69,12 @@ importers: | ||
| 69 | '@voko/access': | 69 | '@voko/access': |
| 70 | specifier: workspace:* | 70 | specifier: workspace:* |
| 71 | version: link:../../packages/voko-access | 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 | dotenv: | 78 | dotenv: |
| 73 | specifier: 16.6.1 | 79 | specifier: 16.6.1 |
| 74 | version: 16.6.1 | 80 | version: 16.6.1 |
| @@ -286,6 +292,10 @@ importers: | @@ -286,6 +292,10 @@ importers: | ||
| 286 | specifier: ^2.1.4 | 292 | specifier: ^2.1.4 |
| 287 | version: 2.1.9(@types/node@24.13.2)(jsdom@25.0.1)(sass@1.101.0) | 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 | packages: | 299 | packages: |
| 290 | 300 | ||
| 291 | '@asamuzakjp/css-color@3.2.0': | 301 | '@asamuzakjp/css-color@3.2.0': |