weifuwu 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/README.zh.md +155 -0
- package/cli.ts +6 -2
- package/dist/agent/rest.d.ts +2 -0
- package/dist/agent/run.d.ts +3 -0
- package/dist/cli.js +6 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +597 -253
- package/dist/logdb/client.d.ts +2 -0
- package/dist/logdb/index.d.ts +2 -0
- package/dist/logdb/migrate.d.ts +5 -0
- package/dist/logdb/rest.d.ts +5 -0
- package/dist/logdb/types.d.ts +27 -0
- package/dist/messager/rest.d.ts +4 -0
- package/dist/postgres/schema/columns.d.ts +5 -0
- package/dist/postgres/schema/index.d.ts +4 -2
- package/dist/postgres/schema/table.d.ts +27 -10
- package/dist/postgres/schema/where.d.ts +15 -0
- package/docs/ai.md +1 -1
- package/docs/logdb.md +145 -0
- package/docs/opencode.md +17 -17
- package/docs/postgres.md +115 -20
- package/docs/tsx.md +2 -2
- package/docs/user.md +17 -17
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2081,11 +2081,11 @@ function evaluateExpression(expr, ctx) {
|
|
|
2081
2081
|
let bestIdx = -1;
|
|
2082
2082
|
let bestOp = null;
|
|
2083
2083
|
let bestFn = null;
|
|
2084
|
-
for (const { op, fn } of operators) {
|
|
2085
|
-
const idx = expr.indexOf(
|
|
2084
|
+
for (const { op: op2, fn } of operators) {
|
|
2085
|
+
const idx = expr.indexOf(op2);
|
|
2086
2086
|
if (idx > 0 && (bestIdx === -1 || idx < bestIdx)) {
|
|
2087
2087
|
bestIdx = idx;
|
|
2088
|
-
bestOp =
|
|
2088
|
+
bestOp = op2;
|
|
2089
2089
|
bestFn = fn;
|
|
2090
2090
|
}
|
|
2091
2091
|
}
|
|
@@ -2363,6 +2363,9 @@ function textArray(name) {
|
|
|
2363
2363
|
function vector(name, dims) {
|
|
2364
2364
|
return col(name, `vector(${dims})`);
|
|
2365
2365
|
}
|
|
2366
|
+
function partitionBy(type, column) {
|
|
2367
|
+
return { type: type.toUpperCase(), column };
|
|
2368
|
+
}
|
|
2366
2369
|
function toDDL(col2) {
|
|
2367
2370
|
const parts = [`"${col2.name}"`, col2.sqlType];
|
|
2368
2371
|
if (col2.isPrimaryKey) parts.push("PRIMARY KEY");
|
|
@@ -2376,6 +2379,44 @@ function toDDL(col2) {
|
|
|
2376
2379
|
return parts.join(" ");
|
|
2377
2380
|
}
|
|
2378
2381
|
|
|
2382
|
+
// postgres/schema/where.ts
|
|
2383
|
+
function op(col2, sqlOp, val) {
|
|
2384
|
+
return new SQL([`"${col2}" ${sqlOp} `, ""], [val]);
|
|
2385
|
+
}
|
|
2386
|
+
function eq(col2, val) {
|
|
2387
|
+
return op(col2, "=", val);
|
|
2388
|
+
}
|
|
2389
|
+
function gte(col2, val) {
|
|
2390
|
+
return op(col2, ">=", val);
|
|
2391
|
+
}
|
|
2392
|
+
function lt(col2, val) {
|
|
2393
|
+
return op(col2, "<", val);
|
|
2394
|
+
}
|
|
2395
|
+
function contains(col2, val) {
|
|
2396
|
+
return new SQL([`"${col2}" @> `, ""], [val]);
|
|
2397
|
+
}
|
|
2398
|
+
function combine(conditions, joiner) {
|
|
2399
|
+
if (conditions.length === 0) return new SQL([""], []);
|
|
2400
|
+
const strings = ["("];
|
|
2401
|
+
const values = [];
|
|
2402
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
2403
|
+
if (i > 0) strings[strings.length - 1] += ` ${joiner} `;
|
|
2404
|
+
const s = conditions[i];
|
|
2405
|
+
for (let j = 0; j < s.strings.length; j++) {
|
|
2406
|
+
strings[strings.length - 1] += s.strings[j];
|
|
2407
|
+
if (j < s.values.length) {
|
|
2408
|
+
strings.push("");
|
|
2409
|
+
values.push(s.values[j]);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
strings[strings.length - 1] += ")";
|
|
2414
|
+
return new SQL(strings, values);
|
|
2415
|
+
}
|
|
2416
|
+
function and(...conditions) {
|
|
2417
|
+
return combine(conditions, "AND");
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2379
2420
|
// postgres/schema/table.ts
|
|
2380
2421
|
var Table = class {
|
|
2381
2422
|
tableName;
|
|
@@ -2390,18 +2431,21 @@ var Table = class {
|
|
|
2390
2431
|
auto: col2.isAutoGenerate
|
|
2391
2432
|
}));
|
|
2392
2433
|
}
|
|
2393
|
-
async create(
|
|
2434
|
+
async create(sql4, opts) {
|
|
2394
2435
|
const colDDL = this.columns.map(toDDL);
|
|
2395
|
-
|
|
2436
|
+
let ddl = `CREATE TABLE IF NOT EXISTS "${this.tableName}" (
|
|
2396
2437
|
${colDDL.join(",\n ")}
|
|
2397
2438
|
)`;
|
|
2398
|
-
|
|
2439
|
+
if (opts?.partitionBy) {
|
|
2440
|
+
ddl += ` PARTITION BY ${opts.partitionBy.type} ("${opts.partitionBy.column}")`;
|
|
2441
|
+
}
|
|
2442
|
+
await sql4.unsafe(ddl);
|
|
2399
2443
|
}
|
|
2400
|
-
async drop(
|
|
2444
|
+
async drop(sql4, opts) {
|
|
2401
2445
|
const cascade = opts?.cascade ? " CASCADE" : "";
|
|
2402
|
-
await
|
|
2446
|
+
await sql4.unsafe(`DROP TABLE IF EXISTS "${this.tableName}"${cascade}`);
|
|
2403
2447
|
}
|
|
2404
|
-
async createIndex(
|
|
2448
|
+
async createIndex(sql4, columns, opts) {
|
|
2405
2449
|
const cols = Array.isArray(columns) ? columns : [columns];
|
|
2406
2450
|
const name = `"${this.tableName}_${cols.join("_")}${opts?.unique ? "_uidx" : "_idx"}"`;
|
|
2407
2451
|
const unique = opts?.unique ? "UNIQUE" : "";
|
|
@@ -2409,13 +2453,13 @@ var Table = class {
|
|
|
2409
2453
|
const colList = cols.map((c) => opts?.desc ? `"${c}" DESC` : `"${c}"`).join(", ");
|
|
2410
2454
|
const operator = opts?.operator ? ` ${opts.operator}` : "";
|
|
2411
2455
|
const ddl = `CREATE ${unique} INDEX IF NOT EXISTS ${name} ON "${this.tableName}" ${using} (${colList}${operator})`.replace(/\s+/g, " ");
|
|
2412
|
-
await
|
|
2456
|
+
await sql4.unsafe(ddl);
|
|
2413
2457
|
}
|
|
2414
|
-
async createUniqueIndex(
|
|
2415
|
-
await this.createIndex(
|
|
2458
|
+
async createUniqueIndex(sql4, columns) {
|
|
2459
|
+
await this.createIndex(sql4, columns, { unique: true });
|
|
2416
2460
|
}
|
|
2417
2461
|
// --- CRUD ---
|
|
2418
|
-
async insert(
|
|
2462
|
+
async insert(sql4, data) {
|
|
2419
2463
|
const filtered = {};
|
|
2420
2464
|
for (const { prop, db, auto } of this.colEntries) {
|
|
2421
2465
|
if (auto) continue;
|
|
@@ -2423,30 +2467,70 @@ var Table = class {
|
|
|
2423
2467
|
filtered[db] = data[prop];
|
|
2424
2468
|
}
|
|
2425
2469
|
}
|
|
2426
|
-
const [row] = await
|
|
2427
|
-
INSERT INTO ${
|
|
2470
|
+
const [row] = await sql4`
|
|
2471
|
+
INSERT INTO ${sql4(this.tableName)} ${sql4(filtered)} RETURNING *
|
|
2428
2472
|
`;
|
|
2429
2473
|
return row;
|
|
2430
2474
|
}
|
|
2431
|
-
async
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
2475
|
+
async insertMany(sql4, data) {
|
|
2476
|
+
const filtered = [];
|
|
2477
|
+
for (const item of data) {
|
|
2478
|
+
const row = {};
|
|
2479
|
+
for (const { prop, db, auto } of this.colEntries) {
|
|
2480
|
+
if (auto) continue;
|
|
2481
|
+
if (prop in item) {
|
|
2482
|
+
row[db] = item[prop];
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
filtered.push(row);
|
|
2486
|
+
}
|
|
2487
|
+
const rows = await sql4`
|
|
2488
|
+
INSERT INTO ${sql4(this.tableName)} ${sql4(filtered)} RETURNING *
|
|
2489
|
+
`;
|
|
2490
|
+
return rows;
|
|
2491
|
+
}
|
|
2492
|
+
async read(sql4, id2) {
|
|
2493
|
+
const [row] = await sql4`
|
|
2494
|
+
SELECT * FROM ${sql4(this.tableName)}
|
|
2495
|
+
WHERE ${sql4("id")} = ${id2} LIMIT 1
|
|
2435
2496
|
`;
|
|
2436
2497
|
return row ?? void 0;
|
|
2437
2498
|
}
|
|
2438
|
-
async
|
|
2499
|
+
async readMany(sql4, where, opts) {
|
|
2439
2500
|
const conditions = [];
|
|
2440
2501
|
const values = [];
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2502
|
+
let w = where;
|
|
2503
|
+
if (Array.isArray(w)) {
|
|
2504
|
+
w = w.length > 0 ? and(...w) : void 0;
|
|
2505
|
+
}
|
|
2506
|
+
if (w instanceof SQL) {
|
|
2507
|
+
let fragment = "";
|
|
2508
|
+
for (let i = 0; i < w.strings.length; i++) {
|
|
2509
|
+
fragment += w.strings[i];
|
|
2510
|
+
if (i < w.values.length) {
|
|
2511
|
+
fragment += `$${values.length + 1}`;
|
|
2512
|
+
values.push(w.values[i]);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
conditions.push(fragment);
|
|
2516
|
+
} else {
|
|
2517
|
+
for (const [prop, value] of Object.entries(w || {})) {
|
|
2518
|
+
if (value === void 0) continue;
|
|
2519
|
+
const entry = this.colEntries.find((e) => e.prop === prop);
|
|
2520
|
+
const db = entry ? entry.db : prop;
|
|
2521
|
+
conditions.push(`"${db}" = $${conditions.length + 1}`);
|
|
2522
|
+
values.push(value);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
2526
|
+
const [countRow] = await sql4.unsafe(`SELECT COUNT(*) AS _total FROM "${this.tableName}"${whereClause}`, values);
|
|
2527
|
+
const count = Number(countRow._total);
|
|
2528
|
+
if (conditions.length === 0 && !opts?.orderBy && !opts?.limit && !opts?.offset && !opts?.select) {
|
|
2529
|
+
const rows2 = await sql4`SELECT * FROM ${sql4(this.tableName)}`;
|
|
2530
|
+
return { count, data: rows2 };
|
|
2447
2531
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2532
|
+
const columns = opts?.select?.length ? opts.select.map((c) => `"${c}"`).join(", ") : "*";
|
|
2533
|
+
let query = `SELECT ${columns} FROM "${this.tableName}"${whereClause}`;
|
|
2450
2534
|
if (opts?.orderBy) {
|
|
2451
2535
|
const orders = Object.entries(opts.orderBy).map(([prop, dir]) => {
|
|
2452
2536
|
const entry = this.colEntries.find((e) => e.prop === prop);
|
|
@@ -2456,14 +2540,29 @@ var Table = class {
|
|
|
2456
2540
|
}
|
|
2457
2541
|
if (opts?.limit) query += ` LIMIT ${opts.limit}`;
|
|
2458
2542
|
if (opts?.offset) query += ` OFFSET ${opts.offset}`;
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2543
|
+
const rows = await sql4.unsafe(query, values);
|
|
2544
|
+
return { count, data: rows };
|
|
2545
|
+
}
|
|
2546
|
+
async update(sql4, id2, data) {
|
|
2547
|
+
const sets = [];
|
|
2548
|
+
const setValues = [];
|
|
2549
|
+
for (const { prop, db } of this.colEntries) {
|
|
2550
|
+
if (prop in data && data[prop] !== void 0) {
|
|
2551
|
+
const val = data[prop];
|
|
2552
|
+
if (val instanceof SQL) {
|
|
2553
|
+
sets.push(`"${db}" = ${val.toSQL()}`);
|
|
2554
|
+
} else {
|
|
2555
|
+
sets.push(`"${db}" = $${sets.length + 1}`);
|
|
2556
|
+
setValues.push(val);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2462
2559
|
}
|
|
2463
|
-
|
|
2464
|
-
|
|
2560
|
+
if (sets.length === 0) return void 0;
|
|
2561
|
+
const query = `UPDATE "${this.tableName}" AS t SET ${sets.join(", ")} FROM (SELECT ctid FROM "${this.tableName}" WHERE id = $${setValues.length + 1} LIMIT 1) AS sub WHERE t.ctid = sub.ctid RETURNING t.*`;
|
|
2562
|
+
const rows = await sql4.unsafe(query, [...setValues, id2]);
|
|
2563
|
+
return rows[0] ?? void 0;
|
|
2465
2564
|
}
|
|
2466
|
-
async
|
|
2565
|
+
async updateMany(sql4, where, data) {
|
|
2467
2566
|
const sets = [];
|
|
2468
2567
|
const setValues = [];
|
|
2469
2568
|
for (const { prop, db } of this.colEntries) {
|
|
@@ -2486,12 +2585,14 @@ var Table = class {
|
|
|
2486
2585
|
wConditions.push(`"${db}" = $${values.length + 1}`);
|
|
2487
2586
|
values.push(value);
|
|
2488
2587
|
}
|
|
2489
|
-
if (sets.length === 0 || wConditions.length === 0) return
|
|
2490
|
-
const
|
|
2491
|
-
|
|
2492
|
-
|
|
2588
|
+
if (sets.length === 0 || wConditions.length === 0) return 0;
|
|
2589
|
+
const rows = await sql4.unsafe(
|
|
2590
|
+
`UPDATE "${this.tableName}" SET ${sets.join(", ")} WHERE ${wConditions.join(" AND ")} RETURNING 1`,
|
|
2591
|
+
values
|
|
2592
|
+
);
|
|
2593
|
+
return rows.length;
|
|
2493
2594
|
}
|
|
2494
|
-
async
|
|
2595
|
+
async deleteMany(sql4, where) {
|
|
2495
2596
|
const conditions = [];
|
|
2496
2597
|
const values = [];
|
|
2497
2598
|
for (const [prop, value] of Object.entries(where)) {
|
|
@@ -2501,18 +2602,26 @@ var Table = class {
|
|
|
2501
2602
|
conditions.push(`"${db}" = $${conditions.length + 1}`);
|
|
2502
2603
|
values.push(value);
|
|
2503
2604
|
}
|
|
2504
|
-
if (conditions.length === 0) return
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2507
|
-
|
|
2605
|
+
if (conditions.length === 0) return 0;
|
|
2606
|
+
const rows = await sql4.unsafe(
|
|
2607
|
+
`DELETE FROM "${this.tableName}" WHERE ${conditions.join(" AND ")} RETURNING 1`,
|
|
2608
|
+
values
|
|
2609
|
+
);
|
|
2610
|
+
return rows.length;
|
|
2611
|
+
}
|
|
2612
|
+
async delete(sql4, id2) {
|
|
2613
|
+
const [row] = await sql4`
|
|
2614
|
+
DELETE FROM ${sql4(this.tableName)} WHERE ${sql4("id")} = ${id2} RETURNING *
|
|
2615
|
+
`;
|
|
2616
|
+
return row ?? void 0;
|
|
2508
2617
|
}
|
|
2509
2618
|
};
|
|
2510
2619
|
var BoundTable = class {
|
|
2511
2620
|
inner;
|
|
2512
2621
|
sql;
|
|
2513
|
-
constructor(
|
|
2622
|
+
constructor(sql4, tableName, builders) {
|
|
2514
2623
|
this.inner = new Table(tableName, builders);
|
|
2515
|
-
this.sql =
|
|
2624
|
+
this.sql = sql4;
|
|
2516
2625
|
}
|
|
2517
2626
|
async create() {
|
|
2518
2627
|
await this.inner.create(this.sql);
|
|
@@ -2529,17 +2638,26 @@ var BoundTable = class {
|
|
|
2529
2638
|
async insert(data) {
|
|
2530
2639
|
return await this.inner.insert(this.sql, data);
|
|
2531
2640
|
}
|
|
2532
|
-
async
|
|
2533
|
-
return await this.inner.
|
|
2641
|
+
async insertMany(data) {
|
|
2642
|
+
return await this.inner.insertMany(this.sql, data);
|
|
2643
|
+
}
|
|
2644
|
+
async read(id2) {
|
|
2645
|
+
return await this.inner.read(this.sql, id2);
|
|
2646
|
+
}
|
|
2647
|
+
async readMany(where, opts) {
|
|
2648
|
+
return await this.inner.readMany(this.sql, where, opts);
|
|
2649
|
+
}
|
|
2650
|
+
async update(id2, data) {
|
|
2651
|
+
return await this.inner.update(this.sql, id2, data);
|
|
2534
2652
|
}
|
|
2535
|
-
async
|
|
2536
|
-
return await this.inner.
|
|
2653
|
+
async updateMany(where, data) {
|
|
2654
|
+
return await this.inner.updateMany(this.sql, where, data);
|
|
2537
2655
|
}
|
|
2538
|
-
async
|
|
2539
|
-
return await this.inner.
|
|
2656
|
+
async delete(id2) {
|
|
2657
|
+
return await this.inner.delete(this.sql, id2);
|
|
2540
2658
|
}
|
|
2541
|
-
async
|
|
2542
|
-
return await this.inner.
|
|
2659
|
+
async deleteMany(where) {
|
|
2660
|
+
return await this.inner.deleteMany(this.sql, where);
|
|
2543
2661
|
}
|
|
2544
2662
|
};
|
|
2545
2663
|
function pgTable(tableName, builders) {
|
|
@@ -2555,7 +2673,7 @@ function postgres(opts) {
|
|
|
2555
2673
|
"postgres: DATABASE_URL is not set. Pass a connection string or set the DATABASE_URL environment variable."
|
|
2556
2674
|
);
|
|
2557
2675
|
}
|
|
2558
|
-
const
|
|
2676
|
+
const sql4 = postgresFactory(connection, {
|
|
2559
2677
|
max: options.max,
|
|
2560
2678
|
ssl: options.ssl,
|
|
2561
2679
|
idle_timeout: options.idle_timeout,
|
|
@@ -2563,24 +2681,24 @@ function postgres(opts) {
|
|
|
2563
2681
|
});
|
|
2564
2682
|
if (options.signal) {
|
|
2565
2683
|
options.signal.addEventListener("abort", () => {
|
|
2566
|
-
|
|
2684
|
+
sql4.end();
|
|
2567
2685
|
}, { once: true });
|
|
2568
2686
|
}
|
|
2569
2687
|
const closeTimeout = options.closeTimeout ?? 5;
|
|
2570
2688
|
const mw = ((req, ctx, next) => {
|
|
2571
|
-
ctx.sql =
|
|
2689
|
+
ctx.sql = sql4;
|
|
2572
2690
|
return next(req, ctx);
|
|
2573
2691
|
});
|
|
2574
|
-
mw.sql =
|
|
2692
|
+
mw.sql = sql4;
|
|
2575
2693
|
mw.table = ((tableName, builders) => {
|
|
2576
|
-
return new BoundTable(
|
|
2694
|
+
return new BoundTable(sql4, tableName, builders);
|
|
2577
2695
|
});
|
|
2578
2696
|
mw.migrate = async () => {
|
|
2579
2697
|
};
|
|
2580
2698
|
mw.transaction = (async (fn) => {
|
|
2581
|
-
return await
|
|
2699
|
+
return await sql4.begin(fn);
|
|
2582
2700
|
});
|
|
2583
|
-
mw.close = () =>
|
|
2701
|
+
mw.close = () => sql4.end({ timeout: closeTimeout });
|
|
2584
2702
|
return mw;
|
|
2585
2703
|
}
|
|
2586
2704
|
|
|
@@ -2894,7 +3012,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
2894
3012
|
}
|
|
2895
3013
|
}
|
|
2896
3014
|
await pg.sql`UPDATE "_oauth2_codes" SET "used" = TRUE WHERE "id" = ${stored.id}`;
|
|
2897
|
-
const user2 = await users.
|
|
3015
|
+
const user2 = await users.read(stored.user_id);
|
|
2898
3016
|
if (!user2) {
|
|
2899
3017
|
return Response.json({ error: "invalid_grant" }, { status: 400 });
|
|
2900
3018
|
}
|
|
@@ -2982,7 +3100,7 @@ function user(options) {
|
|
|
2982
3100
|
if (oauth2Enabled) {
|
|
2983
3101
|
oauth2 = createOAuth2Server({ pg, users, jwtSecret: secret, expiresIn });
|
|
2984
3102
|
}
|
|
2985
|
-
async function
|
|
3103
|
+
async function migrate7() {
|
|
2986
3104
|
await migrate({ pg, usersTable: table, oauth2: oauth2Enabled });
|
|
2987
3105
|
}
|
|
2988
3106
|
function signToken(user2) {
|
|
@@ -2997,11 +3115,11 @@ function user(options) {
|
|
|
2997
3115
|
return user2;
|
|
2998
3116
|
}
|
|
2999
3117
|
async function findByEmail(email) {
|
|
3000
|
-
const rows = await users.
|
|
3118
|
+
const { data: rows } = await users.readMany({ email });
|
|
3001
3119
|
return rows[0];
|
|
3002
3120
|
}
|
|
3003
3121
|
async function findById(id2) {
|
|
3004
|
-
return await users.
|
|
3122
|
+
return await users.read(id2);
|
|
3005
3123
|
}
|
|
3006
3124
|
async function register(data) {
|
|
3007
3125
|
const { email, password, name } = RegisterSchema.parse(data);
|
|
@@ -3019,7 +3137,7 @@ function user(options) {
|
|
|
3019
3137
|
}
|
|
3020
3138
|
async function login(data) {
|
|
3021
3139
|
const { email, password } = LoginSchema.parse(data);
|
|
3022
|
-
const rows = await users.
|
|
3140
|
+
const { data: rows } = await users.readMany({ email });
|
|
3023
3141
|
const row = rows[0];
|
|
3024
3142
|
if (!row) {
|
|
3025
3143
|
const err = new Error("Invalid email or password");
|
|
@@ -3101,7 +3219,7 @@ function user(options) {
|
|
|
3101
3219
|
const mod = {
|
|
3102
3220
|
router,
|
|
3103
3221
|
middleware,
|
|
3104
|
-
migrate:
|
|
3222
|
+
migrate: migrate7,
|
|
3105
3223
|
register,
|
|
3106
3224
|
login,
|
|
3107
3225
|
verify,
|
|
@@ -3284,14 +3402,14 @@ function queue(opts) {
|
|
|
3284
3402
|
|
|
3285
3403
|
// tenant/migrate.ts
|
|
3286
3404
|
async function migrate2(opts) {
|
|
3287
|
-
const { sql:
|
|
3288
|
-
await
|
|
3405
|
+
const { sql: sql4 } = opts;
|
|
3406
|
+
await sql4.unsafe(`CREATE EXTENSION IF NOT EXISTS "vector"`);
|
|
3289
3407
|
const tenants = pgTable("_tenants", {
|
|
3290
3408
|
id: text("id").primaryKey().default(sql`gen_random_uuid()`),
|
|
3291
3409
|
name: text("name").notNull(),
|
|
3292
3410
|
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
3293
3411
|
});
|
|
3294
|
-
await tenants.create(
|
|
3412
|
+
await tenants.create(sql4);
|
|
3295
3413
|
const members = pgTable("_tenant_members", {
|
|
3296
3414
|
id: serial("id").primaryKey(),
|
|
3297
3415
|
tenant_id: text("tenant_id").notNull().references("_tenants", "id", "cascade"),
|
|
@@ -3299,9 +3417,9 @@ async function migrate2(opts) {
|
|
|
3299
3417
|
role: text("role").notNull().default("member"),
|
|
3300
3418
|
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
3301
3419
|
});
|
|
3302
|
-
await members.create(
|
|
3303
|
-
await members.createIndex(
|
|
3304
|
-
await
|
|
3420
|
+
await members.create(sql4);
|
|
3421
|
+
await members.createIndex(sql4, "user_id");
|
|
3422
|
+
await sql4.unsafe(`CREATE UNIQUE INDEX IF NOT EXISTS "_tenant_members_unique_idx" ON "_tenant_members" ("tenant_id", "user_id")`);
|
|
3305
3423
|
const tables = pgTable("_user_tables", {
|
|
3306
3424
|
id: serial("id").primaryKey(),
|
|
3307
3425
|
tenant_id: text("tenant_id").notNull().references("_tenants", "id", "cascade"),
|
|
@@ -3310,9 +3428,9 @@ async function migrate2(opts) {
|
|
|
3310
3428
|
fields: jsonb("fields").notNull().default(sql`'[]'::jsonb`),
|
|
3311
3429
|
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
3312
3430
|
});
|
|
3313
|
-
await tables.create(
|
|
3314
|
-
await tables.createIndex(
|
|
3315
|
-
await
|
|
3431
|
+
await tables.create(sql4);
|
|
3432
|
+
await tables.createIndex(sql4, "tenant_id");
|
|
3433
|
+
await sql4.unsafe(`CREATE UNIQUE INDEX IF NOT EXISTS "_user_tables_unique_idx" ON "_user_tables" ("tenant_id", "slug")`);
|
|
3316
3434
|
}
|
|
3317
3435
|
|
|
3318
3436
|
// tenant/rest.ts
|
|
@@ -3506,8 +3624,8 @@ function zodType(field) {
|
|
|
3506
3624
|
}
|
|
3507
3625
|
return t;
|
|
3508
3626
|
}
|
|
3509
|
-
async function getUserTable(
|
|
3510
|
-
const [row] = await
|
|
3627
|
+
async function getUserTable(sql4, tenantId, slug) {
|
|
3628
|
+
const [row] = await sql4`
|
|
3511
3629
|
SELECT * FROM "_user_tables"
|
|
3512
3630
|
WHERE tenant_id = ${tenantId} AND slug = ${slug}
|
|
3513
3631
|
LIMIT 1
|
|
@@ -3518,24 +3636,24 @@ function requireAdmin(ctx) {
|
|
|
3518
3636
|
if (ctx.tenant?.role !== "admin") return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
3519
3637
|
return null;
|
|
3520
3638
|
}
|
|
3521
|
-
function buildRouter(
|
|
3639
|
+
function buildRouter(sql4, usersTable) {
|
|
3522
3640
|
const r = new Router();
|
|
3523
3641
|
r.post("/sys/tenants", async (req, ctx) => {
|
|
3524
3642
|
const { name } = await req.json();
|
|
3525
3643
|
if (!name || typeof name !== "string") {
|
|
3526
3644
|
return Response.json({ error: "name is required" }, { status: 400 });
|
|
3527
3645
|
}
|
|
3528
|
-
const [tenant2] = await
|
|
3646
|
+
const [tenant2] = await sql4`
|
|
3529
3647
|
INSERT INTO "_tenants" ("name") VALUES (${name}) RETURNING *
|
|
3530
3648
|
`;
|
|
3531
|
-
await
|
|
3649
|
+
await sql4`
|
|
3532
3650
|
INSERT INTO "_tenant_members" ("tenant_id", "user_id", "role")
|
|
3533
3651
|
VALUES (${tenant2.id}, ${ctx.user.id}, 'admin')
|
|
3534
3652
|
`;
|
|
3535
3653
|
return Response.json(tenant2, { status: 201 });
|
|
3536
3654
|
});
|
|
3537
3655
|
r.get("/sys/tenants", async (_req, ctx) => {
|
|
3538
|
-
const rows = await
|
|
3656
|
+
const rows = await sql4`
|
|
3539
3657
|
SELECT t.*, tm.role FROM "_tenants" t
|
|
3540
3658
|
JOIN "_tenant_members" tm ON tm.tenant_id = t.id
|
|
3541
3659
|
WHERE tm.user_id = ${ctx.user.id}
|
|
@@ -3546,16 +3664,16 @@ function buildRouter(sql2, usersTable) {
|
|
|
3546
3664
|
const err = requireAdmin(ctx);
|
|
3547
3665
|
if (err) return err;
|
|
3548
3666
|
const { email, role = "member" } = await req.json();
|
|
3549
|
-
const [user2] = await
|
|
3550
|
-
SELECT id FROM ${
|
|
3667
|
+
const [user2] = await sql4`
|
|
3668
|
+
SELECT id FROM ${sql4(usersTable)} WHERE "email" = ${email} LIMIT 1
|
|
3551
3669
|
`;
|
|
3552
3670
|
if (!user2) return Response.json({ error: "User not found" }, { status: 404 });
|
|
3553
|
-
const [existing] = await
|
|
3671
|
+
const [existing] = await sql4`
|
|
3554
3672
|
SELECT id FROM "_tenant_members"
|
|
3555
3673
|
WHERE tenant_id = ${ctx.tenant.id} AND user_id = ${user2.id} LIMIT 1
|
|
3556
3674
|
`;
|
|
3557
3675
|
if (existing) return Response.json({ error: "Already a member" }, { status: 409 });
|
|
3558
|
-
await
|
|
3676
|
+
await sql4`
|
|
3559
3677
|
INSERT INTO "_tenant_members" ("tenant_id", "user_id", "role")
|
|
3560
3678
|
VALUES (${ctx.tenant.id}, ${user2.id}, ${role})
|
|
3561
3679
|
`;
|
|
@@ -3565,7 +3683,7 @@ function buildRouter(sql2, usersTable) {
|
|
|
3565
3683
|
const err = requireAdmin(ctx);
|
|
3566
3684
|
if (err) return err;
|
|
3567
3685
|
const userId = parseInt(ctx.params.userId, 10);
|
|
3568
|
-
await
|
|
3686
|
+
await sql4`
|
|
3569
3687
|
DELETE FROM "_tenant_members"
|
|
3570
3688
|
WHERE tenant_id = ${ctx.tenant.id} AND user_id = ${userId}
|
|
3571
3689
|
`;
|
|
@@ -3581,17 +3699,17 @@ function buildRouter(sql2, usersTable) {
|
|
|
3581
3699
|
if (fieldErrs.length) {
|
|
3582
3700
|
return Response.json({ error: "Invalid fields", details: fieldErrs }, { status: 400 });
|
|
3583
3701
|
}
|
|
3584
|
-
const [existing] = await
|
|
3702
|
+
const [existing] = await sql4`
|
|
3585
3703
|
SELECT id FROM "_user_tables"
|
|
3586
3704
|
WHERE tenant_id = ${ctx.tenant.id} AND slug = ${body.slug} LIMIT 1
|
|
3587
3705
|
`;
|
|
3588
3706
|
if (existing) return Response.json({ error: "Table already exists" }, { status: 409 });
|
|
3589
3707
|
const createSQL = createTableSQL(ctx.tenant.id, body.slug, body.fields);
|
|
3590
|
-
await
|
|
3708
|
+
await sql4.unsafe(createSQL);
|
|
3591
3709
|
for (const stmt of createIndexesSQL(ctx.tenant.id, body.slug, body.fields)) {
|
|
3592
|
-
await
|
|
3710
|
+
await sql4.unsafe(stmt);
|
|
3593
3711
|
}
|
|
3594
|
-
const [row] = await
|
|
3712
|
+
const [row] = await sql4`
|
|
3595
3713
|
INSERT INTO "_user_tables" ("tenant_id", "slug", "label", "fields")
|
|
3596
3714
|
VALUES (${ctx.tenant.id}, ${body.slug}, ${body.label || ""}, ${body.fields})
|
|
3597
3715
|
RETURNING *
|
|
@@ -3599,7 +3717,7 @@ function buildRouter(sql2, usersTable) {
|
|
|
3599
3717
|
return Response.json(row, { status: 201 });
|
|
3600
3718
|
});
|
|
3601
3719
|
r.get("/sys/tables", async (_req, ctx) => {
|
|
3602
|
-
const rows = await
|
|
3720
|
+
const rows = await sql4`
|
|
3603
3721
|
SELECT * FROM "_user_tables"
|
|
3604
3722
|
WHERE tenant_id = ${ctx.tenant.id}
|
|
3605
3723
|
ORDER BY created_at DESC
|
|
@@ -3607,7 +3725,7 @@ function buildRouter(sql2, usersTable) {
|
|
|
3607
3725
|
return Response.json(rows);
|
|
3608
3726
|
});
|
|
3609
3727
|
r.get("/sys/tables/:slug", async (_req, ctx) => {
|
|
3610
|
-
const table = await getUserTable(
|
|
3728
|
+
const table = await getUserTable(sql4, ctx.tenant.id, ctx.params.slug);
|
|
3611
3729
|
if (!table) return Response.json({ error: "Table not found" }, { status: 404 });
|
|
3612
3730
|
return Response.json(table);
|
|
3613
3731
|
});
|
|
@@ -3618,15 +3736,15 @@ function buildRouter(sql2, usersTable) {
|
|
|
3618
3736
|
if (!body.fields || !Array.isArray(body.fields)) {
|
|
3619
3737
|
return Response.json({ error: "fields array required" }, { status: 400 });
|
|
3620
3738
|
}
|
|
3621
|
-
const table = await getUserTable(
|
|
3739
|
+
const table = await getUserTable(sql4, ctx.tenant.id, ctx.params.slug);
|
|
3622
3740
|
if (!table) return Response.json({ error: "Table not found" }, { status: 404 });
|
|
3623
3741
|
const existingNames = new Set(table.fields.map((f) => f.name));
|
|
3624
3742
|
const newFields = body.fields.filter((f) => !existingNames.has(f.name));
|
|
3625
3743
|
for (const f of newFields) {
|
|
3626
|
-
await
|
|
3744
|
+
await sql4.unsafe(addColumnSQL(ctx.tenant.id, ctx.params.slug, f));
|
|
3627
3745
|
}
|
|
3628
3746
|
const merged = [...table.fields, ...newFields];
|
|
3629
|
-
await
|
|
3747
|
+
await sql4`
|
|
3630
3748
|
UPDATE "_user_tables"
|
|
3631
3749
|
SET fields = ${merged}
|
|
3632
3750
|
WHERE id = ${table.id}
|
|
@@ -3636,15 +3754,15 @@ function buildRouter(sql2, usersTable) {
|
|
|
3636
3754
|
r.delete("/sys/tables/:slug", async (_req, ctx) => {
|
|
3637
3755
|
const err = requireAdmin(ctx);
|
|
3638
3756
|
if (err) return err;
|
|
3639
|
-
await
|
|
3640
|
-
await
|
|
3757
|
+
await sql4.unsafe(dropTableSQL(ctx.tenant.id, ctx.params.slug));
|
|
3758
|
+
await sql4`
|
|
3641
3759
|
DELETE FROM "_user_tables"
|
|
3642
3760
|
WHERE tenant_id = ${ctx.tenant.id} AND slug = ${ctx.params.slug}
|
|
3643
3761
|
`;
|
|
3644
3762
|
return Response.json({ ok: true });
|
|
3645
3763
|
});
|
|
3646
3764
|
async function resolveTable(ctx) {
|
|
3647
|
-
return getUserTable(
|
|
3765
|
+
return getUserTable(sql4, ctx.tenant.id, ctx.params["_slug"]);
|
|
3648
3766
|
}
|
|
3649
3767
|
function internalName(ctx) {
|
|
3650
3768
|
return internalTableName(ctx.tenant.id, ctx.params["_slug"]);
|
|
@@ -3667,11 +3785,11 @@ function buildRouter(sql2, usersTable) {
|
|
|
3667
3785
|
try {
|
|
3668
3786
|
const parsed = JSON.parse(searchVector);
|
|
3669
3787
|
const [rows2, countResult2] = await Promise.all([
|
|
3670
|
-
|
|
3788
|
+
sql4.unsafe(
|
|
3671
3789
|
`SELECT *, "${searchField}" ${operator} $1::vector AS "_distance" FROM "${name2}" WHERE tenant_id = $2 ORDER BY "_distance" LIMIT $3 OFFSET $4`,
|
|
3672
3790
|
[parsed, ctx.tenant.id, limit, offset]
|
|
3673
3791
|
),
|
|
3674
|
-
|
|
3792
|
+
sql4.unsafe(
|
|
3675
3793
|
`SELECT count(*) as count FROM "${name2}" WHERE tenant_id = $1`,
|
|
3676
3794
|
[ctx.tenant.id]
|
|
3677
3795
|
)
|
|
@@ -3683,11 +3801,11 @@ function buildRouter(sql2, usersTable) {
|
|
|
3683
3801
|
}
|
|
3684
3802
|
const name = internalName(ctx);
|
|
3685
3803
|
const [rows, countResult] = await Promise.all([
|
|
3686
|
-
|
|
3804
|
+
sql4.unsafe(
|
|
3687
3805
|
`SELECT * FROM "${name}" WHERE tenant_id = $1 ORDER BY "${orderCol}" ${orderDir} LIMIT $2 OFFSET $3`,
|
|
3688
3806
|
[ctx.tenant.id, limit, offset]
|
|
3689
3807
|
),
|
|
3690
|
-
|
|
3808
|
+
sql4.unsafe(
|
|
3691
3809
|
`SELECT count(*) as count FROM "${name}" WHERE tenant_id = $1`,
|
|
3692
3810
|
[ctx.tenant.id]
|
|
3693
3811
|
)
|
|
@@ -3707,15 +3825,15 @@ function buildRouter(sql2, usersTable) {
|
|
|
3707
3825
|
parsed.tenant_id = ctx.tenant.id;
|
|
3708
3826
|
delete parsed.id;
|
|
3709
3827
|
const name = internalName(ctx);
|
|
3710
|
-
const [row] = await
|
|
3828
|
+
const [row] = await sql4`INSERT INTO ${sql4(name)} ${sql4(parsed)} RETURNING *`;
|
|
3711
3829
|
return Response.json(row, { status: 201 });
|
|
3712
3830
|
});
|
|
3713
3831
|
r.get("/:_slug/:id", async (_req, ctx) => {
|
|
3714
3832
|
const table = await resolveTable(ctx);
|
|
3715
3833
|
if (!table) return Response.json({ error: "Table not found" }, { status: 404 });
|
|
3716
3834
|
const name = internalName(ctx);
|
|
3717
|
-
const [row] = await
|
|
3718
|
-
SELECT * FROM ${
|
|
3835
|
+
const [row] = await sql4`
|
|
3836
|
+
SELECT * FROM ${sql4(name)}
|
|
3719
3837
|
WHERE id = ${parseInt(ctx.params.id, 10)} AND tenant_id = ${ctx.tenant.id}
|
|
3720
3838
|
LIMIT 1
|
|
3721
3839
|
`;
|
|
@@ -3736,16 +3854,16 @@ function buildRouter(sql2, usersTable) {
|
|
|
3736
3854
|
delete parsed.tenant_id;
|
|
3737
3855
|
if (Object.keys(parsed).length === 0) {
|
|
3738
3856
|
const name2 = internalName(ctx);
|
|
3739
|
-
const [row2] = await
|
|
3740
|
-
SELECT * FROM ${
|
|
3857
|
+
const [row2] = await sql4`
|
|
3858
|
+
SELECT * FROM ${sql4(name2)}
|
|
3741
3859
|
WHERE id = ${parseInt(ctx.params.id, 10)} AND tenant_id = ${ctx.tenant.id}
|
|
3742
3860
|
LIMIT 1
|
|
3743
3861
|
`;
|
|
3744
3862
|
return Response.json(row2 ?? { error: "Not found" }, { status: row2 ? 200 : 404 });
|
|
3745
3863
|
}
|
|
3746
3864
|
const name = internalName(ctx);
|
|
3747
|
-
const [row] = await
|
|
3748
|
-
UPDATE ${
|
|
3865
|
+
const [row] = await sql4`
|
|
3866
|
+
UPDATE ${sql4(name)} SET ${sql4(parsed)}
|
|
3749
3867
|
WHERE id = ${parseInt(ctx.params.id, 10)} AND tenant_id = ${ctx.tenant.id}
|
|
3750
3868
|
RETURNING *
|
|
3751
3869
|
`;
|
|
@@ -3754,8 +3872,8 @@ function buildRouter(sql2, usersTable) {
|
|
|
3754
3872
|
});
|
|
3755
3873
|
r.delete("/:_slug/:id", async (_req, ctx) => {
|
|
3756
3874
|
const name = internalName(ctx);
|
|
3757
|
-
const result = await
|
|
3758
|
-
DELETE FROM ${
|
|
3875
|
+
const result = await sql4`
|
|
3876
|
+
DELETE FROM ${sql4(name)}
|
|
3759
3877
|
WHERE id = ${parseInt(ctx.params.id, 10)} AND tenant_id = ${ctx.tenant.id}
|
|
3760
3878
|
RETURNING 1
|
|
3761
3879
|
`;
|
|
@@ -3766,11 +3884,11 @@ function buildRouter(sql2, usersTable) {
|
|
|
3766
3884
|
});
|
|
3767
3885
|
async function handleNested(req, ctx, method) {
|
|
3768
3886
|
const [parentTable, nestedSlug] = await Promise.all([
|
|
3769
|
-
getUserTable(
|
|
3887
|
+
getUserTable(sql4, ctx.tenant.id, ctx.params["_slug"]),
|
|
3770
3888
|
ctx.params["_nested"]
|
|
3771
3889
|
]);
|
|
3772
3890
|
if (!parentTable) return Response.json({ error: "Parent table not found" }, { status: 404 });
|
|
3773
|
-
const childTable = await getUserTable(
|
|
3891
|
+
const childTable = await getUserTable(sql4, ctx.tenant.id, nestedSlug);
|
|
3774
3892
|
if (!childTable) return Response.json({ error: "Nested table not found" }, { status: 404 });
|
|
3775
3893
|
const relField = findRelation(childTable.fields, ctx.params["_slug"]);
|
|
3776
3894
|
if (!relField) {
|
|
@@ -3780,18 +3898,18 @@ function buildRouter(sql2, usersTable) {
|
|
|
3780
3898
|
if (relFields.length === 2) {
|
|
3781
3899
|
const otherRel = relFields.find((f) => f.name !== relField.name);
|
|
3782
3900
|
const targetSlug = otherRel.relation.table;
|
|
3783
|
-
const targetTable = await getUserTable(
|
|
3901
|
+
const targetTable = await getUserTable(sql4, ctx.tenant.id, targetSlug);
|
|
3784
3902
|
if (!targetTable) return Response.json({ error: "Target table not found" }, { status: 404 });
|
|
3785
3903
|
const childName2 = internalTableName(ctx.tenant.id, nestedSlug);
|
|
3786
3904
|
const targetName = internalTableName(ctx.tenant.id, targetSlug);
|
|
3787
3905
|
const parentId2 = parseInt(ctx.params.id, 10);
|
|
3788
3906
|
if (method === "GET") {
|
|
3789
3907
|
const [rows, countResult] = await Promise.all([
|
|
3790
|
-
|
|
3908
|
+
sql4.unsafe(
|
|
3791
3909
|
`SELECT t.* FROM "${targetName}" t JOIN "${childName2}" j ON j."${otherRel.name}" = t.id WHERE j."${relField.name}" = $1 AND t.tenant_id = $2 ORDER BY t.id DESC`,
|
|
3792
3910
|
[parentId2, ctx.tenant.id]
|
|
3793
3911
|
),
|
|
3794
|
-
|
|
3912
|
+
sql4.unsafe(
|
|
3795
3913
|
`SELECT count(*) as count FROM "${targetName}" t JOIN "${childName2}" j ON j."${otherRel.name}" = t.id WHERE j."${relField.name}" = $1 AND t.tenant_id = $2`,
|
|
3796
3914
|
[parentId2, ctx.tenant.id]
|
|
3797
3915
|
)
|
|
@@ -3804,11 +3922,11 @@ function buildRouter(sql2, usersTable) {
|
|
|
3804
3922
|
const parentId = parseInt(ctx.params.id, 10);
|
|
3805
3923
|
if (method === "GET") {
|
|
3806
3924
|
const [rows, countResult] = await Promise.all([
|
|
3807
|
-
|
|
3925
|
+
sql4.unsafe(
|
|
3808
3926
|
`SELECT * FROM "${childName}" WHERE "${relField.name}" = $1 AND tenant_id = $2 ORDER BY id DESC`,
|
|
3809
3927
|
[parentId, ctx.tenant.id]
|
|
3810
3928
|
),
|
|
3811
|
-
|
|
3929
|
+
sql4.unsafe(
|
|
3812
3930
|
`SELECT count(*) as count FROM "${childName}" WHERE "${relField.name}" = $1 AND tenant_id = $2`,
|
|
3813
3931
|
[parentId, ctx.tenant.id]
|
|
3814
3932
|
)
|
|
@@ -3825,7 +3943,7 @@ function buildRouter(sql2, usersTable) {
|
|
|
3825
3943
|
parsed.tenant_id = ctx.tenant.id;
|
|
3826
3944
|
parsed[relField.name] = parentId;
|
|
3827
3945
|
delete parsed.id;
|
|
3828
|
-
const [row] = await
|
|
3946
|
+
const [row] = await sql4`INSERT INTO ${sql4(childName)} ${sql4(parsed)} RETURNING *`;
|
|
3829
3947
|
return Response.json(row, { status: 201 });
|
|
3830
3948
|
}
|
|
3831
3949
|
r.get("/:_slug/:id/:_nested", async (req, ctx) => handleNested(req, ctx, "GET"));
|
|
@@ -4067,15 +4185,15 @@ function buildMutationFields(tables, ctx) {
|
|
|
4067
4185
|
}
|
|
4068
4186
|
return fields;
|
|
4069
4187
|
}
|
|
4070
|
-
function buildGraphQLHandler(
|
|
4188
|
+
function buildGraphQLHandler(sql4) {
|
|
4071
4189
|
const r = new Router();
|
|
4072
4190
|
r.post("/", async (req, ctx) => {
|
|
4073
|
-
const tables = await
|
|
4191
|
+
const tables = await sql4`
|
|
4074
4192
|
SELECT * FROM "_user_tables"
|
|
4075
4193
|
WHERE tenant_id = ${ctx.tenant.id}
|
|
4076
4194
|
ORDER BY created_at ASC
|
|
4077
4195
|
`;
|
|
4078
|
-
const buildCtx = { sql:
|
|
4196
|
+
const buildCtx = { sql: sql4, tenantId: ctx.tenant.id, tables };
|
|
4079
4197
|
const schema = new GraphQLSchema({
|
|
4080
4198
|
query: new GraphQLObjectType({
|
|
4081
4199
|
name: "Query",
|
|
@@ -4110,12 +4228,12 @@ function buildGraphQLHandler(sql2) {
|
|
|
4110
4228
|
});
|
|
4111
4229
|
});
|
|
4112
4230
|
async function handleGET(req, _ctx) {
|
|
4113
|
-
const tables = await
|
|
4231
|
+
const tables = await sql4`
|
|
4114
4232
|
SELECT * FROM "_user_tables"
|
|
4115
4233
|
WHERE tenant_id = ${_ctx.tenant.id}
|
|
4116
4234
|
ORDER BY created_at ASC
|
|
4117
4235
|
`;
|
|
4118
|
-
const buildCtx = { sql:
|
|
4236
|
+
const buildCtx = { sql: sql4, tenantId: _ctx.tenant.id, tables };
|
|
4119
4237
|
const schema = new GraphQLSchema({
|
|
4120
4238
|
query: new GraphQLObjectType({
|
|
4121
4239
|
name: "Query",
|
|
@@ -4145,18 +4263,18 @@ function buildGraphQLHandler(sql2) {
|
|
|
4145
4263
|
// tenant/client.ts
|
|
4146
4264
|
function tenant(options) {
|
|
4147
4265
|
const pg = options.pg;
|
|
4148
|
-
const
|
|
4266
|
+
const sql4 = pg.sql;
|
|
4149
4267
|
const usersTable = options.usersTable;
|
|
4150
4268
|
const base = new PgModule(pg);
|
|
4151
4269
|
return {
|
|
4152
|
-
migrate: () => migrate2({ sql:
|
|
4270
|
+
migrate: () => migrate2({ sql: sql4, usersTable }),
|
|
4153
4271
|
middleware() {
|
|
4154
4272
|
return async (req, ctx, next) => {
|
|
4155
4273
|
const user2 = ctx.user;
|
|
4156
4274
|
if (!user2) {
|
|
4157
4275
|
return new Response("Unauthorized", { status: 401 });
|
|
4158
4276
|
}
|
|
4159
|
-
const members = await
|
|
4277
|
+
const members = await sql4`
|
|
4160
4278
|
SELECT tm.role, t.id, t.name
|
|
4161
4279
|
FROM "_tenant_members" tm
|
|
4162
4280
|
JOIN "_tenants" t ON t.id = tm.tenant_id
|
|
@@ -4185,8 +4303,8 @@ function tenant(options) {
|
|
|
4185
4303
|
return next(req, ctx);
|
|
4186
4304
|
};
|
|
4187
4305
|
},
|
|
4188
|
-
router: () => buildRouter(
|
|
4189
|
-
graphql: () => buildGraphQLHandler(
|
|
4306
|
+
router: () => buildRouter(sql4, usersTable),
|
|
4307
|
+
graphql: () => buildGraphQLHandler(sql4),
|
|
4190
4308
|
close: () => base.close()
|
|
4191
4309
|
};
|
|
4192
4310
|
}
|
|
@@ -4196,7 +4314,7 @@ import { createOpenAI } from "@ai-sdk/openai";
|
|
|
4196
4314
|
|
|
4197
4315
|
// agent/migrate.ts
|
|
4198
4316
|
async function migrate3(opts) {
|
|
4199
|
-
const { sql:
|
|
4317
|
+
const { sql: sql4, embeddingDimension } = opts;
|
|
4200
4318
|
const agents = pgTable("_agents", {
|
|
4201
4319
|
id: serial("id").primaryKey(),
|
|
4202
4320
|
tenant_id: text("tenant_id"),
|
|
@@ -4210,8 +4328,8 @@ async function migrate3(opts) {
|
|
|
4210
4328
|
created_at: timestamptz("created_at").notNull().default(sql`NOW()`),
|
|
4211
4329
|
updated_at: timestamptz("updated_at").notNull().default(sql`NOW()`)
|
|
4212
4330
|
});
|
|
4213
|
-
await agents.create(
|
|
4214
|
-
await agents.createIndex(
|
|
4331
|
+
await agents.create(sql4);
|
|
4332
|
+
await agents.createIndex(sql4, "tenant_id");
|
|
4215
4333
|
const docs = pgTable("_knowledge_documents", {
|
|
4216
4334
|
id: serial("id").primaryKey(),
|
|
4217
4335
|
agent_id: integer("agent_id").notNull().references("_agents", "id", "cascade"),
|
|
@@ -4221,22 +4339,22 @@ async function migrate3(opts) {
|
|
|
4221
4339
|
metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`),
|
|
4222
4340
|
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
4223
4341
|
});
|
|
4224
|
-
await docs.create(
|
|
4225
|
-
await docs.createIndex(
|
|
4342
|
+
await docs.create(sql4);
|
|
4343
|
+
await docs.createIndex(sql4, "agent_id");
|
|
4226
4344
|
}
|
|
4227
4345
|
|
|
4228
4346
|
// agent/rest.ts
|
|
4229
|
-
async function getAgent(sql2, id2) {
|
|
4230
|
-
const [row] = await sql2`SELECT * FROM "_agents" WHERE id = ${id2} LIMIT 1`;
|
|
4231
|
-
return row ?? null;
|
|
4232
|
-
}
|
|
4233
4347
|
function buildRouter2(deps) {
|
|
4234
|
-
const { sql:
|
|
4348
|
+
const { sql: sql4, agents: agentsTable, runner } = deps;
|
|
4349
|
+
async function getAgent(id2) {
|
|
4350
|
+
const row = await agentsTable.read(id2);
|
|
4351
|
+
return row ?? null;
|
|
4352
|
+
}
|
|
4235
4353
|
const r = new Router();
|
|
4236
4354
|
r.post("/agents", async (req) => {
|
|
4237
4355
|
const body = await req.json();
|
|
4238
4356
|
if (!body.name) return Response.json({ error: "name is required" }, { status: 400 });
|
|
4239
|
-
const [row] = await
|
|
4357
|
+
const [row] = await sql4`
|
|
4240
4358
|
INSERT INTO "_agents" ("name", "description", "type", "model", "system_prompt", "owner_id")
|
|
4241
4359
|
VALUES (${body.name}, ${body.description || ""}, ${body.type || "chat"}, ${body.model || ""}, ${body.system_prompt || ""}, ${body.owner_id || 1})
|
|
4242
4360
|
RETURNING *
|
|
@@ -4244,17 +4362,17 @@ function buildRouter2(deps) {
|
|
|
4244
4362
|
return Response.json(row, { status: 201 });
|
|
4245
4363
|
});
|
|
4246
4364
|
r.get("/agents", async () => {
|
|
4247
|
-
const rows = await
|
|
4365
|
+
const { data: rows } = await agentsTable.readMany(void 0, { orderBy: { created_at: "desc" } });
|
|
4248
4366
|
return Response.json(rows);
|
|
4249
4367
|
});
|
|
4250
4368
|
r.get("/agents/:id", async (_req, ctx) => {
|
|
4251
|
-
const agent2 = await getAgent(
|
|
4369
|
+
const agent2 = await getAgent(parseInt(ctx.params.id, 10));
|
|
4252
4370
|
if (!agent2) return Response.json({ error: "Agent not found" }, { status: 404 });
|
|
4253
4371
|
return Response.json(agent2);
|
|
4254
4372
|
});
|
|
4255
4373
|
r.patch("/agents/:id", async (req, ctx) => {
|
|
4256
4374
|
const id2 = parseInt(ctx.params.id, 10);
|
|
4257
|
-
const agent2 = await getAgent(
|
|
4375
|
+
const agent2 = await getAgent(id2);
|
|
4258
4376
|
if (!agent2) return Response.json({ error: "Agent not found" }, { status: 404 });
|
|
4259
4377
|
const body = await req.json();
|
|
4260
4378
|
const fields = [];
|
|
@@ -4269,7 +4387,7 @@ function buildRouter2(deps) {
|
|
|
4269
4387
|
if (fields.length === 0) return Response.json({ error: "No fields to update" }, { status: 400 });
|
|
4270
4388
|
values.push(id2);
|
|
4271
4389
|
fields.push(`"updated_at" = NOW()`);
|
|
4272
|
-
const [row] = await
|
|
4390
|
+
const [row] = await sql4.unsafe(
|
|
4273
4391
|
`UPDATE "_agents" SET ${fields.join(", ")} WHERE id = $${idx} RETURNING *`,
|
|
4274
4392
|
values
|
|
4275
4393
|
);
|
|
@@ -4277,7 +4395,7 @@ function buildRouter2(deps) {
|
|
|
4277
4395
|
});
|
|
4278
4396
|
r.delete("/agents/:id", async (_req, ctx) => {
|
|
4279
4397
|
const id2 = parseInt(ctx.params.id, 10);
|
|
4280
|
-
const [row] = await
|
|
4398
|
+
const [row] = await sql4`DELETE FROM "_agents" WHERE id = ${id2} RETURNING 1`;
|
|
4281
4399
|
if (!row) return Response.json({ error: "Agent not found" }, { status: 404 });
|
|
4282
4400
|
return Response.json({ ok: true });
|
|
4283
4401
|
});
|
|
@@ -4301,7 +4419,7 @@ function buildRouter2(deps) {
|
|
|
4301
4419
|
});
|
|
4302
4420
|
r.post("/agents/:id/knowledge", async (req, ctx) => {
|
|
4303
4421
|
const agentId = parseInt(ctx.params.id, 10);
|
|
4304
|
-
const agent2 = await getAgent(
|
|
4422
|
+
const agent2 = await getAgent(agentId);
|
|
4305
4423
|
if (!agent2) return Response.json({ error: "Agent not found" }, { status: 404 });
|
|
4306
4424
|
const body = await req.json();
|
|
4307
4425
|
if (!body.content) return Response.json({ error: "content is required" }, { status: 400 });
|
|
@@ -4314,7 +4432,7 @@ function buildRouter2(deps) {
|
|
|
4314
4432
|
});
|
|
4315
4433
|
r.get("/agents/:id/knowledge", async (_req, ctx) => {
|
|
4316
4434
|
const agentId = parseInt(ctx.params.id, 10);
|
|
4317
|
-
const rows = await
|
|
4435
|
+
const rows = await sql4`
|
|
4318
4436
|
SELECT id, title, created_at FROM "_knowledge_documents"
|
|
4319
4437
|
WHERE agent_id = ${agentId}
|
|
4320
4438
|
ORDER BY created_at DESC
|
|
@@ -4324,7 +4442,7 @@ function buildRouter2(deps) {
|
|
|
4324
4442
|
r.delete("/agents/:id/knowledge/:docId", async (_req, ctx) => {
|
|
4325
4443
|
const agentId = parseInt(ctx.params.id, 10);
|
|
4326
4444
|
const docId = parseInt(ctx.params.docId, 10);
|
|
4327
|
-
await
|
|
4445
|
+
await sql4`DELETE FROM "_knowledge_documents" WHERE id = ${docId} AND agent_id = ${agentId}`;
|
|
4328
4446
|
return Response.json({ ok: true });
|
|
4329
4447
|
});
|
|
4330
4448
|
return r;
|
|
@@ -4380,6 +4498,9 @@ function createSSEStream(iterable, opts) {
|
|
|
4380
4498
|
}
|
|
4381
4499
|
|
|
4382
4500
|
// agent/run.ts
|
|
4501
|
+
function hasKnowledgeDocs(sql4, agentId) {
|
|
4502
|
+
return sql4`SELECT 1 FROM "_knowledge_documents" WHERE agent_id = ${agentId} LIMIT 1`.then((r) => r.length > 0);
|
|
4503
|
+
}
|
|
4383
4504
|
function chunkContent(content, chunkSize = 512, overlap = 64) {
|
|
4384
4505
|
const paragraphs = content.split(/\n\n+/);
|
|
4385
4506
|
const chunks = [];
|
|
@@ -4394,31 +4515,28 @@ function chunkContent(content, chunkSize = 512, overlap = 64) {
|
|
|
4394
4515
|
if (current) chunks.push(current);
|
|
4395
4516
|
return chunks;
|
|
4396
4517
|
}
|
|
4397
|
-
function
|
|
4398
|
-
return sql2`SELECT 1 FROM "_knowledge_documents" WHERE agent_id = ${agentId} LIMIT 1`.then((r) => r.length > 0);
|
|
4399
|
-
}
|
|
4400
|
-
async function searchKnowledge(sql2, embedModel, agentId, query, limit = 5) {
|
|
4518
|
+
async function searchKnowledge(sql4, embedModel, agentId, query, limit = 5) {
|
|
4401
4519
|
const { embedding } = await embed({ model: embedModel, value: query });
|
|
4402
4520
|
const vec = `[${embedding.join(",")}]`;
|
|
4403
|
-
const docs = await
|
|
4521
|
+
const docs = await sql4.unsafe(
|
|
4404
4522
|
`SELECT id, title, content, metadata, embedding <=> $1::vector AS _score FROM "_knowledge_documents" WHERE agent_id = $2 ORDER BY embedding <=> $1::vector LIMIT $3`,
|
|
4405
4523
|
[vec, agentId, limit]
|
|
4406
4524
|
);
|
|
4407
4525
|
return docs.map((d) => ({ id: d.id, title: d.title, content: d.content, score: d._score }));
|
|
4408
4526
|
}
|
|
4409
|
-
async function loadAgent(
|
|
4410
|
-
const
|
|
4527
|
+
async function loadAgent(agents, agentId) {
|
|
4528
|
+
const row = await agents.read(agentId);
|
|
4411
4529
|
return row ?? null;
|
|
4412
4530
|
}
|
|
4413
4531
|
function createRunner(deps) {
|
|
4414
|
-
const { sql:
|
|
4532
|
+
const { sql: sql4, agents, getModel, getEmbeddingModel, userTools } = deps;
|
|
4415
4533
|
async function run(agentId, params) {
|
|
4416
|
-
const agent2 = await loadAgent(
|
|
4534
|
+
const agent2 = await loadAgent(agents, agentId);
|
|
4417
4535
|
if (!agent2 || !agent2.active) throw new Error("Agent not found or inactive");
|
|
4418
4536
|
const model = getModel();
|
|
4419
4537
|
const embedModel = getEmbeddingModel();
|
|
4420
4538
|
const start = Date.now();
|
|
4421
|
-
const hasKB = await hasKnowledgeDocs(
|
|
4539
|
+
const hasKB = await hasKnowledgeDocs(sql4, agentId);
|
|
4422
4540
|
const messages2 = params.messages ?? [];
|
|
4423
4541
|
if (messages2.length === 0) {
|
|
4424
4542
|
messages2.push({ role: "user", content: params.input });
|
|
@@ -4432,7 +4550,7 @@ function createRunner(deps) {
|
|
|
4432
4550
|
limit: z4.number().default(5).describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF")
|
|
4433
4551
|
}),
|
|
4434
4552
|
execute: async ({ query, limit }) => {
|
|
4435
|
-
return searchKnowledge(
|
|
4553
|
+
return searchKnowledge(sql4, embedModel, agentId, query, limit);
|
|
4436
4554
|
}
|
|
4437
4555
|
};
|
|
4438
4556
|
}
|
|
@@ -4481,13 +4599,13 @@ function createRunner(deps) {
|
|
|
4481
4599
|
const [first] = chunks;
|
|
4482
4600
|
const { embedding } = await embed({ model: embedModel, value: first });
|
|
4483
4601
|
const vec = `[${embedding.join(",")}]`;
|
|
4484
|
-
const [doc] = await
|
|
4602
|
+
const [doc] = await sql4.unsafe(
|
|
4485
4603
|
`INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector) RETURNING *`,
|
|
4486
4604
|
[agentId, title, first, vec]
|
|
4487
4605
|
);
|
|
4488
4606
|
for (let i = 1; i < chunks.length; i++) {
|
|
4489
4607
|
const { embedding: emb } = await embed({ model: embedModel, value: chunks[i] });
|
|
4490
|
-
await
|
|
4608
|
+
await sql4.unsafe(
|
|
4491
4609
|
`INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector)`,
|
|
4492
4610
|
[agentId, `${title} (${i + 1})`, chunks[i], `[${emb.join(",")}]`]
|
|
4493
4611
|
);
|
|
@@ -4512,7 +4630,7 @@ function createModelsFromEnv() {
|
|
|
4512
4630
|
}
|
|
4513
4631
|
function agent(options) {
|
|
4514
4632
|
const pg = options.pg;
|
|
4515
|
-
const
|
|
4633
|
+
const sql4 = pg.sql;
|
|
4516
4634
|
const model = options.model;
|
|
4517
4635
|
const embeddingModel = options.embeddingModel;
|
|
4518
4636
|
const dimension = options.embeddingDimension ?? 1024;
|
|
@@ -4527,11 +4645,33 @@ function agent(options) {
|
|
|
4527
4645
|
if (!defaultModels) defaultModels = createModelsFromEnv();
|
|
4528
4646
|
return defaultModels.embeddingModel;
|
|
4529
4647
|
}
|
|
4530
|
-
const
|
|
4648
|
+
const agentsTable = pg.table("_agents", {
|
|
4649
|
+
id: serial("id"),
|
|
4650
|
+
tenant_id: text("tenant_id"),
|
|
4651
|
+
name: text("name"),
|
|
4652
|
+
description: text("description"),
|
|
4653
|
+
type: text("type"),
|
|
4654
|
+
model: text("model"),
|
|
4655
|
+
system_prompt: text("system_prompt"),
|
|
4656
|
+
owner_id: integer("owner_id"),
|
|
4657
|
+
active: boolean_("active"),
|
|
4658
|
+
created_at: timestamptz("created_at"),
|
|
4659
|
+
updated_at: timestamptz("updated_at")
|
|
4660
|
+
});
|
|
4661
|
+
const knowledgeTable = pg.table("_knowledge_documents", {
|
|
4662
|
+
id: serial("id"),
|
|
4663
|
+
agent_id: integer("agent_id"),
|
|
4664
|
+
title: text("title"),
|
|
4665
|
+
content: text("content"),
|
|
4666
|
+
embedding: vector("embedding", dimension),
|
|
4667
|
+
metadata: jsonb("metadata"),
|
|
4668
|
+
created_at: timestamptz("created_at")
|
|
4669
|
+
});
|
|
4670
|
+
const runner = createRunner({ sql: sql4, agents: agentsTable, knowledge: knowledgeTable, getModel, getEmbeddingModel, userTools: options.tools });
|
|
4531
4671
|
const base = new PgModule(pg);
|
|
4532
4672
|
return {
|
|
4533
|
-
migrate: () => migrate3({ sql:
|
|
4534
|
-
router: () => buildRouter2({ sql:
|
|
4673
|
+
migrate: () => migrate3({ sql: sql4, embeddingDimension: dimension }),
|
|
4674
|
+
router: () => buildRouter2({ sql: sql4, agents: agentsTable, runner }),
|
|
4535
4675
|
run: (agentId, params) => runner.run(agentId, params),
|
|
4536
4676
|
addKnowledge: (agentId, title, content) => runner.addKnowledge(agentId, title, content),
|
|
4537
4677
|
close: () => base.close()
|
|
@@ -4539,7 +4679,7 @@ function agent(options) {
|
|
|
4539
4679
|
}
|
|
4540
4680
|
|
|
4541
4681
|
// messager/migrate.ts
|
|
4542
|
-
async function migrate4(
|
|
4682
|
+
async function migrate4(sql4) {
|
|
4543
4683
|
const channels2 = pgTable("_channels", {
|
|
4544
4684
|
id: serial("id").primaryKey(),
|
|
4545
4685
|
tenant_id: text("tenant_id"),
|
|
@@ -4548,8 +4688,8 @@ async function migrate4(sql2) {
|
|
|
4548
4688
|
created_by: integer("created_by").notNull(),
|
|
4549
4689
|
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
4550
4690
|
});
|
|
4551
|
-
await channels2.create(
|
|
4552
|
-
await channels2.createIndex(
|
|
4691
|
+
await channels2.create(sql4);
|
|
4692
|
+
await channels2.createIndex(sql4, "tenant_id");
|
|
4553
4693
|
const members = pgTable("_channel_members", {
|
|
4554
4694
|
id: serial("id").primaryKey(),
|
|
4555
4695
|
channel_id: integer("channel_id").notNull().references("_channels", "id", "cascade"),
|
|
@@ -4559,9 +4699,9 @@ async function migrate4(sql2) {
|
|
|
4559
4699
|
last_read_id: integer("last_read_id"),
|
|
4560
4700
|
last_read_at: timestamptz("last_read_at")
|
|
4561
4701
|
});
|
|
4562
|
-
await members.create(
|
|
4563
|
-
await members.createIndex(
|
|
4564
|
-
await
|
|
4702
|
+
await members.create(sql4);
|
|
4703
|
+
await members.createIndex(sql4, "member_id");
|
|
4704
|
+
await sql4.unsafe(`CREATE UNIQUE INDEX IF NOT EXISTS "_channel_members_unique_idx" ON "_channel_members" ("channel_id", "member_id", "member_type")`);
|
|
4565
4705
|
const messages2 = pgTable("_messages", {
|
|
4566
4706
|
id: serial("id").primaryKey(),
|
|
4567
4707
|
channel_id: integer("channel_id").notNull().references("_channels", "id", "cascade"),
|
|
@@ -4575,8 +4715,8 @@ async function migrate4(sql2) {
|
|
|
4575
4715
|
mime_type: text("mime_type"),
|
|
4576
4716
|
created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
|
|
4577
4717
|
});
|
|
4578
|
-
await messages2.create(
|
|
4579
|
-
await messages2.createIndex(
|
|
4718
|
+
await messages2.create(sql4);
|
|
4719
|
+
await messages2.createIndex(sql4, ["channel_id", "created_at"], { desc: true });
|
|
4580
4720
|
}
|
|
4581
4721
|
|
|
4582
4722
|
// messager/ws.ts
|
|
@@ -4613,7 +4753,7 @@ function unsubscribe(ws) {
|
|
|
4613
4753
|
}
|
|
4614
4754
|
}
|
|
4615
4755
|
function createWSHandler(deps) {
|
|
4616
|
-
const { sql:
|
|
4756
|
+
const { sql: sql4, agents } = deps;
|
|
4617
4757
|
return {
|
|
4618
4758
|
open(ws, ctx) {
|
|
4619
4759
|
const userId = ctx.user?.id;
|
|
@@ -4636,7 +4776,7 @@ function createWSHandler(deps) {
|
|
|
4636
4776
|
switch (type) {
|
|
4637
4777
|
case "message": {
|
|
4638
4778
|
if (!content || !channel_id) return;
|
|
4639
|
-
const [row] = await
|
|
4779
|
+
const [row] = await sql4`
|
|
4640
4780
|
INSERT INTO "_messages" ("channel_id", "sender_id", "sender_type", "content")
|
|
4641
4781
|
VALUES (${channel_id}, ${userId}, 'user', ${content})
|
|
4642
4782
|
RETURNING *
|
|
@@ -4645,14 +4785,14 @@ function createWSHandler(deps) {
|
|
|
4645
4785
|
broadcastToChannel(channel_id, { type: "message", data: message });
|
|
4646
4786
|
subscribe(ws, userId, channel_id);
|
|
4647
4787
|
if (agents) {
|
|
4648
|
-
const agentMembers = await
|
|
4788
|
+
const agentMembers = await sql4`
|
|
4649
4789
|
SELECT member_id FROM "_channel_members"
|
|
4650
4790
|
WHERE channel_id = ${channel_id} AND member_type = 'agent'
|
|
4651
4791
|
`;
|
|
4652
4792
|
for (const am of agentMembers) {
|
|
4653
4793
|
agents.run(am.member_id, { input: content, stream: false }).then((result) => {
|
|
4654
4794
|
if ("output" in result && result.output) {
|
|
4655
|
-
|
|
4795
|
+
sql4`
|
|
4656
4796
|
INSERT INTO "_messages" ("channel_id", "sender_id", "sender_type", "content")
|
|
4657
4797
|
VALUES (${channel_id}, ${am.member_id}, 'agent', ${result.output})
|
|
4658
4798
|
`.then(([r]) => {
|
|
@@ -4681,7 +4821,7 @@ function createWSHandler(deps) {
|
|
|
4681
4821
|
case "read": {
|
|
4682
4822
|
if (!channel_id || !last_message_id) return;
|
|
4683
4823
|
subscribe(ws, userId, channel_id);
|
|
4684
|
-
await
|
|
4824
|
+
await sql4`
|
|
4685
4825
|
UPDATE "_channel_members"
|
|
4686
4826
|
SET last_read_id = ${last_message_id}, last_read_at = NOW()
|
|
4687
4827
|
WHERE channel_id = ${channel_id} AND member_id = ${userId} AND member_type = 'user'
|
|
@@ -4707,24 +4847,24 @@ function createWSHandler(deps) {
|
|
|
4707
4847
|
|
|
4708
4848
|
// messager/rest.ts
|
|
4709
4849
|
function buildRouter3(deps) {
|
|
4710
|
-
const { sql:
|
|
4850
|
+
const { sql: sql4, channels: channels2, members, messages: messages2, agents } = deps;
|
|
4711
4851
|
const r = new Router();
|
|
4712
4852
|
r.post("/channels", async (req) => {
|
|
4713
4853
|
const body = await req.json();
|
|
4714
4854
|
if (!body.name) return Response.json({ error: "name is required" }, { status: 400 });
|
|
4715
|
-
const [ch] = await
|
|
4855
|
+
const [ch] = await sql4`
|
|
4716
4856
|
INSERT INTO "_channels" ("name", "type", "created_by")
|
|
4717
4857
|
VALUES (${body.name}, ${body.type || "channel"}, ${body.created_by || 1})
|
|
4718
4858
|
RETURNING *
|
|
4719
4859
|
`;
|
|
4720
4860
|
const channel = ch;
|
|
4721
|
-
await
|
|
4861
|
+
await sql4`
|
|
4722
4862
|
INSERT INTO "_channel_members" ("channel_id", "member_id", "member_type", "role")
|
|
4723
4863
|
VALUES (${channel.id}, ${channel.created_by}, 'user', 'admin')
|
|
4724
4864
|
`;
|
|
4725
4865
|
if (Array.isArray(body.members)) {
|
|
4726
4866
|
for (const m of body.members) {
|
|
4727
|
-
await
|
|
4867
|
+
await sql4`
|
|
4728
4868
|
INSERT INTO "_channel_members" ("channel_id", "member_id", "member_type", "role")
|
|
4729
4869
|
VALUES (${channel.id}, ${m.member_id ?? m}, ${m.member_type ?? "user"}, ${m.role ?? "member"})
|
|
4730
4870
|
ON CONFLICT DO NOTHING
|
|
@@ -4735,7 +4875,7 @@ function buildRouter3(deps) {
|
|
|
4735
4875
|
});
|
|
4736
4876
|
r.get("/channels", async (_req, ctx) => {
|
|
4737
4877
|
const userId = ctx.user?.id ?? 1;
|
|
4738
|
-
const rows = await
|
|
4878
|
+
const rows = await sql4`
|
|
4739
4879
|
SELECT c.*, (
|
|
4740
4880
|
SELECT content FROM "_messages"
|
|
4741
4881
|
WHERE channel_id = c.id
|
|
@@ -4750,22 +4890,20 @@ function buildRouter3(deps) {
|
|
|
4750
4890
|
});
|
|
4751
4891
|
r.get("/channels/:id", async (_req, ctx) => {
|
|
4752
4892
|
const id2 = parseInt(ctx.params.id, 10);
|
|
4753
|
-
const
|
|
4893
|
+
const ch = await channels2.read(id2);
|
|
4754
4894
|
if (!ch) return Response.json({ error: "Channel not found" }, { status: 404 });
|
|
4755
|
-
const
|
|
4756
|
-
|
|
4757
|
-
`;
|
|
4758
|
-
return Response.json({ channel: ch, members });
|
|
4895
|
+
const { data: memberRows } = await members.readMany({ channel_id: id2 });
|
|
4896
|
+
return Response.json({ channel: ch, members: memberRows });
|
|
4759
4897
|
});
|
|
4760
4898
|
r.delete("/channels/:id", async (_req, ctx) => {
|
|
4761
4899
|
const id2 = parseInt(ctx.params.id, 10);
|
|
4762
|
-
await
|
|
4900
|
+
await sql4`DELETE FROM "_channels" WHERE id = ${id2}`;
|
|
4763
4901
|
return Response.json({ ok: true });
|
|
4764
4902
|
});
|
|
4765
4903
|
r.post("/channels/:id/members", async (req, ctx) => {
|
|
4766
4904
|
const channelId = parseInt(ctx.params.id, 10);
|
|
4767
4905
|
const body = await req.json();
|
|
4768
|
-
await
|
|
4906
|
+
await sql4`
|
|
4769
4907
|
INSERT INTO "_channel_members" ("channel_id", "member_id", "member_type", "role")
|
|
4770
4908
|
VALUES (${channelId}, ${body.member_id}, ${body.member_type || "user"}, ${body.role || "member"})
|
|
4771
4909
|
ON CONFLICT DO NOTHING
|
|
@@ -4775,7 +4913,7 @@ function buildRouter3(deps) {
|
|
|
4775
4913
|
r.delete("/channels/:id/members/:memberId", async (_req, ctx) => {
|
|
4776
4914
|
const channelId = parseInt(ctx.params.id, 10);
|
|
4777
4915
|
const memberId = parseInt(ctx.params.memberId, 10);
|
|
4778
|
-
await
|
|
4916
|
+
await sql4`
|
|
4779
4917
|
DELETE FROM "_channel_members"
|
|
4780
4918
|
WHERE channel_id = ${channelId} AND member_id = ${memberId}
|
|
4781
4919
|
`;
|
|
@@ -4787,25 +4925,23 @@ function buildRouter3(deps) {
|
|
|
4787
4925
|
const limit = Math.min(parseInt(url.searchParams.get("limit") || "50", 10), 100);
|
|
4788
4926
|
const before = url.searchParams.get("before");
|
|
4789
4927
|
if (before) {
|
|
4790
|
-
const rows2 = await
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
`;
|
|
4928
|
+
const { data: rows2 } = await messages2.readMany(
|
|
4929
|
+
[eq("channel_id", channelId), lt("id", parseInt(before, 10))],
|
|
4930
|
+
{ orderBy: { created_at: "desc" }, limit }
|
|
4931
|
+
);
|
|
4795
4932
|
return Response.json({ rows: rows2.reverse(), count: rows2.length });
|
|
4796
4933
|
}
|
|
4797
|
-
const rows = await
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
`;
|
|
4934
|
+
const { data: rows } = await messages2.readMany(
|
|
4935
|
+
{ channel_id: channelId },
|
|
4936
|
+
{ orderBy: { created_at: "desc" }, limit }
|
|
4937
|
+
);
|
|
4802
4938
|
return Response.json({ rows: rows.reverse(), count: rows.length });
|
|
4803
4939
|
});
|
|
4804
4940
|
r.post("/channels/:id/messages", async (req, ctx) => {
|
|
4805
4941
|
const channelId = parseInt(ctx.params.id, 10);
|
|
4806
4942
|
const body = await req.json();
|
|
4807
4943
|
if (!body.content) return Response.json({ error: "content is required" }, { status: 400 });
|
|
4808
|
-
const [row] = await
|
|
4944
|
+
const [row] = await sql4`
|
|
4809
4945
|
INSERT INTO "_messages" ("channel_id", "sender_id", "sender_type", "type", "content")
|
|
4810
4946
|
VALUES (${channelId}, ${body.sender_id ?? 1}, ${body.sender_type || "user"}, ${body.type || "text"}, ${body.content})
|
|
4811
4947
|
RETURNING *
|
|
@@ -4813,14 +4949,14 @@ function buildRouter3(deps) {
|
|
|
4813
4949
|
const msg = row;
|
|
4814
4950
|
broadcastToChannel(channelId, { type: "message", data: msg });
|
|
4815
4951
|
if (agents) {
|
|
4816
|
-
const agentMembers = await
|
|
4952
|
+
const agentMembers = await sql4`
|
|
4817
4953
|
SELECT member_id FROM "_channel_members"
|
|
4818
4954
|
WHERE channel_id = ${channelId} AND member_type = 'agent'
|
|
4819
4955
|
`;
|
|
4820
4956
|
for (const am of agentMembers) {
|
|
4821
4957
|
agents.run(am.member_id, { input: body.content, stream: false }).then((result) => {
|
|
4822
4958
|
if ("output" in result && result.output) {
|
|
4823
|
-
|
|
4959
|
+
sql4`
|
|
4824
4960
|
INSERT INTO "_messages" ("channel_id", "sender_id", "sender_type", "content")
|
|
4825
4961
|
VALUES (${channelId}, ${am.member_id}, 'agent', ${result.output})
|
|
4826
4962
|
`.then(([r2]) => {
|
|
@@ -4840,7 +4976,7 @@ function buildRouter3(deps) {
|
|
|
4840
4976
|
const channelId = parseInt(ctx.params.id, 10);
|
|
4841
4977
|
const body = await req.json();
|
|
4842
4978
|
const userId = body.user_id ?? ctx.user?.id ?? 1;
|
|
4843
|
-
await
|
|
4979
|
+
await sql4`
|
|
4844
4980
|
UPDATE "_channel_members"
|
|
4845
4981
|
SET last_read_id = ${body.last_message_id}, last_read_at = NOW()
|
|
4846
4982
|
WHERE channel_id = ${channelId} AND member_id = ${userId} AND member_type = 'user'
|
|
@@ -4857,15 +4993,45 @@ function buildRouter3(deps) {
|
|
|
4857
4993
|
// messager/client.ts
|
|
4858
4994
|
function messager(options) {
|
|
4859
4995
|
const pg = options.pg;
|
|
4860
|
-
const
|
|
4996
|
+
const sql4 = pg.sql;
|
|
4861
4997
|
const agents = options.agents;
|
|
4862
4998
|
const base = new PgModule(pg);
|
|
4999
|
+
const channels2 = pg.table("_channels", {
|
|
5000
|
+
id: serial("id"),
|
|
5001
|
+
tenant_id: text("tenant_id"),
|
|
5002
|
+
name: text("name"),
|
|
5003
|
+
type: text("type"),
|
|
5004
|
+
created_by: integer("created_by"),
|
|
5005
|
+
created_at: timestamptz("created_at")
|
|
5006
|
+
});
|
|
5007
|
+
const members = pg.table("_channel_members", {
|
|
5008
|
+
id: serial("id"),
|
|
5009
|
+
channel_id: integer("channel_id"),
|
|
5010
|
+
member_id: integer("member_id"),
|
|
5011
|
+
member_type: text("member_type"),
|
|
5012
|
+
role: text("role"),
|
|
5013
|
+
last_read_id: integer("last_read_id"),
|
|
5014
|
+
last_read_at: timestamptz("last_read_at")
|
|
5015
|
+
});
|
|
5016
|
+
const messages2 = pg.table("_messages", {
|
|
5017
|
+
id: serial("id"),
|
|
5018
|
+
channel_id: integer("channel_id"),
|
|
5019
|
+
sender_id: integer("sender_id"),
|
|
5020
|
+
sender_type: text("sender_type"),
|
|
5021
|
+
type: text("type"),
|
|
5022
|
+
content: text("content"),
|
|
5023
|
+
file_url: text("file_url"),
|
|
5024
|
+
file_name: text("file_name"),
|
|
5025
|
+
file_size: integer("file_size"),
|
|
5026
|
+
mime_type: text("mime_type"),
|
|
5027
|
+
created_at: timestamptz("created_at")
|
|
5028
|
+
});
|
|
4863
5029
|
return {
|
|
4864
|
-
migrate: () => migrate4(
|
|
4865
|
-
router: () => buildRouter3({ sql:
|
|
4866
|
-
wsHandler: () => createWSHandler({ sql:
|
|
5030
|
+
migrate: () => migrate4(sql4),
|
|
5031
|
+
router: () => buildRouter3({ sql: sql4, channels: channels2, members, messages: messages2, agents }),
|
|
5032
|
+
wsHandler: () => createWSHandler({ sql: sql4, agents }),
|
|
4867
5033
|
async send(channelId, content, opts) {
|
|
4868
|
-
const [row] = await
|
|
5034
|
+
const [row] = await sql4`
|
|
4869
5035
|
INSERT INTO "_messages" ("channel_id", "sender_id", "sender_type", "type", "content")
|
|
4870
5036
|
VALUES (${channelId}, ${opts?.sender_id ?? 0}, ${opts?.sender_type ?? "system"}, ${opts?.type ?? "text"}, ${content})
|
|
4871
5037
|
RETURNING *
|
|
@@ -5432,7 +5598,7 @@ function ensureCertificates(config) {
|
|
|
5432
5598
|
import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
|
|
5433
5599
|
|
|
5434
5600
|
// opencode/migrate.ts
|
|
5435
|
-
async function migrate5(
|
|
5601
|
+
async function migrate5(sql4) {
|
|
5436
5602
|
const sessions2 = pgTable("_opencode_sessions", {
|
|
5437
5603
|
id: uuid("id").default(sql`gen_random_uuid()`).primaryKey(),
|
|
5438
5604
|
tenant_id: text("tenant_id"),
|
|
@@ -5447,8 +5613,8 @@ async function migrate5(sql2) {
|
|
|
5447
5613
|
created_at: timestamptz("created_at").default(sql`NOW()`),
|
|
5448
5614
|
updated_at: timestamptz("updated_at").default(sql`NOW()`)
|
|
5449
5615
|
});
|
|
5450
|
-
await sessions2.create(
|
|
5451
|
-
await sessions2.createIndex(
|
|
5616
|
+
await sessions2.create(sql4);
|
|
5617
|
+
await sessions2.createIndex(sql4, "user_id");
|
|
5452
5618
|
const messages2 = pgTable("_opencode_messages", {
|
|
5453
5619
|
id: serial("id").primaryKey(),
|
|
5454
5620
|
session_id: uuid("session_id").notNull().references("_opencode_sessions", "id", "cascade"),
|
|
@@ -5460,8 +5626,8 @@ async function migrate5(sql2) {
|
|
|
5460
5626
|
tokens_out: integer("tokens_out").default(0),
|
|
5461
5627
|
created_at: timestamptz("created_at").default(sql`NOW()`)
|
|
5462
5628
|
});
|
|
5463
|
-
await messages2.create(
|
|
5464
|
-
await messages2.createIndex(
|
|
5629
|
+
await messages2.create(sql4);
|
|
5630
|
+
await messages2.createIndex(sql4, ["session_id", "created_at"]);
|
|
5465
5631
|
}
|
|
5466
5632
|
|
|
5467
5633
|
// opencode/session.ts
|
|
@@ -5490,11 +5656,11 @@ var messages = pgTable("_opencode_messages", {
|
|
|
5490
5656
|
tokens_out: integer("tokens_out"),
|
|
5491
5657
|
created_at: timestamptz("created_at")
|
|
5492
5658
|
});
|
|
5493
|
-
async function createSession(
|
|
5659
|
+
async function createSession(sql4, opts, cwd, mountPath) {
|
|
5494
5660
|
const id2 = randomUUID2();
|
|
5495
5661
|
const ws = computeSessionWorkspace(cwd, mountPath, id2);
|
|
5496
5662
|
await mkdir2(ws, { recursive: true });
|
|
5497
|
-
const [row] = await
|
|
5663
|
+
const [row] = await sql4`
|
|
5498
5664
|
INSERT INTO "_opencode_sessions" ("id", "user_id", "title", "model", "workspace", "system_prompt")
|
|
5499
5665
|
VALUES (${id2}, ${opts.userId ?? 0}, ${opts.title ?? null}, ${opts.model ?? "deepseek-v4-flash"}, ${ws}, ${opts.systemPrompt ?? null})
|
|
5500
5666
|
RETURNING *
|
|
@@ -5505,39 +5671,39 @@ function computeSessionWorkspace(cwd, mountPath, sessionId) {
|
|
|
5505
5671
|
const name = !mountPath || mountPath === "/" ? "default" : mountPath.replace(/^\//, "");
|
|
5506
5672
|
return join3(cwd, ".sessions", name, sessionId);
|
|
5507
5673
|
}
|
|
5508
|
-
async function getSession(
|
|
5509
|
-
const rows = await sessions.
|
|
5674
|
+
async function getSession(sql4, id2) {
|
|
5675
|
+
const { data: rows } = await sessions.readMany(sql4, { id: id2, active: true });
|
|
5510
5676
|
return rows[0] ?? null;
|
|
5511
5677
|
}
|
|
5512
|
-
async function listSessions(
|
|
5678
|
+
async function listSessions(sql4, userId) {
|
|
5513
5679
|
const opts = { orderBy: { updated_at: "desc" } };
|
|
5514
5680
|
if (userId !== void 0) {
|
|
5515
|
-
const rows2 = await sessions.
|
|
5681
|
+
const { data: rows2 } = await sessions.readMany(sql4, { user_id: userId, active: true }, opts);
|
|
5516
5682
|
return rows2;
|
|
5517
5683
|
}
|
|
5518
|
-
const rows = await sessions.
|
|
5684
|
+
const { data: rows } = await sessions.readMany(sql4, { active: true }, opts);
|
|
5519
5685
|
return rows;
|
|
5520
5686
|
}
|
|
5521
|
-
async function deleteSession(
|
|
5522
|
-
await sessions.update(
|
|
5687
|
+
async function deleteSession(sql4, id2) {
|
|
5688
|
+
await sessions.update(sql4, id2, { active: false, updated_at: sql`NOW()` });
|
|
5523
5689
|
}
|
|
5524
|
-
async function getHistory(
|
|
5525
|
-
const rows = await messages.
|
|
5690
|
+
async function getHistory(sql4, sessionId, limit = 50) {
|
|
5691
|
+
const { data: rows } = await messages.readMany(sql4, { session_id: sessionId }, {
|
|
5526
5692
|
orderBy: { created_at: "asc" },
|
|
5527
5693
|
limit
|
|
5528
5694
|
});
|
|
5529
5695
|
return rows;
|
|
5530
5696
|
}
|
|
5531
|
-
async function addTextMessage(
|
|
5532
|
-
const [row] = await
|
|
5697
|
+
async function addTextMessage(sql4, sessionId, role, content, tokensIn = 0, tokensOut = 0) {
|
|
5698
|
+
const [row] = await sql4`
|
|
5533
5699
|
INSERT INTO "_opencode_messages" ("session_id", "role", "content", "tokens_in", "tokens_out")
|
|
5534
5700
|
VALUES (${sessionId}, ${role}, ${content}, ${tokensIn}, ${tokensOut})
|
|
5535
5701
|
RETURNING *
|
|
5536
5702
|
`;
|
|
5537
5703
|
return row;
|
|
5538
5704
|
}
|
|
5539
|
-
async function addToolMessages(
|
|
5540
|
-
const [row] = await
|
|
5705
|
+
async function addToolMessages(sql4, sessionId, toolCalls, toolResults) {
|
|
5706
|
+
const [row] = await sql4`
|
|
5541
5707
|
INSERT INTO "_opencode_messages" ("session_id", "role", "tool_calls", "tool_results")
|
|
5542
5708
|
VALUES (${sessionId}, 'tool', ${JSON.stringify(toolCalls)}, ${JSON.stringify(toolResults)})
|
|
5543
5709
|
RETURNING *
|
|
@@ -5548,7 +5714,7 @@ async function addToolMessages(sql2, sessionId, toolCalls, toolResults) {
|
|
|
5548
5714
|
// opencode/run.ts
|
|
5549
5715
|
import { streamText as streamText2, stepCountIs } from "ai";
|
|
5550
5716
|
async function* executeGenerator(opts) {
|
|
5551
|
-
const { sessionId, input, model, tools, systemPrompt, messages: messages2, sql:
|
|
5717
|
+
const { sessionId, input, model, tools, systemPrompt, messages: messages2, sql: sql4, abortSignal } = opts;
|
|
5552
5718
|
const lastStepToolCalls = [];
|
|
5553
5719
|
let currentAssistantText = "";
|
|
5554
5720
|
let currentUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
@@ -5577,7 +5743,7 @@ async function* executeGenerator(opts) {
|
|
|
5577
5743
|
}
|
|
5578
5744
|
if (step.toolResults.length > 0) {
|
|
5579
5745
|
try {
|
|
5580
|
-
await addToolMessages(
|
|
5746
|
+
await addToolMessages(sql4, sessionId, lastStepToolCalls, step.toolResults);
|
|
5581
5747
|
} catch (e) {
|
|
5582
5748
|
console.error("[opencode] save tool messages failed:", e);
|
|
5583
5749
|
}
|
|
@@ -5591,7 +5757,7 @@ async function* executeGenerator(opts) {
|
|
|
5591
5757
|
totalTokens: result2.usage?.totalTokens ?? 0
|
|
5592
5758
|
};
|
|
5593
5759
|
try {
|
|
5594
|
-
await addTextMessage(
|
|
5760
|
+
await addTextMessage(sql4, sessionId, "assistant", currentAssistantText, currentUsage.promptTokens, currentUsage.completionTokens);
|
|
5595
5761
|
} catch (e) {
|
|
5596
5762
|
console.error("[opencode] save message failed:", e);
|
|
5597
5763
|
}
|
|
@@ -6024,11 +6190,11 @@ function createTools(ctx) {
|
|
|
6024
6190
|
|
|
6025
6191
|
// opencode/rest.ts
|
|
6026
6192
|
async function buildRouter4(deps) {
|
|
6027
|
-
const { sql:
|
|
6193
|
+
const { sql: sql4, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
|
|
6028
6194
|
const router = new Router();
|
|
6029
6195
|
router.post("/sessions", async (req, ctx) => {
|
|
6030
6196
|
const body = await req.json().catch(() => ({}));
|
|
6031
|
-
const session = await createSession(
|
|
6197
|
+
const session = await createSession(sql4, {
|
|
6032
6198
|
title: body.title,
|
|
6033
6199
|
model: body.model,
|
|
6034
6200
|
systemPrompt: body.systemPrompt || systemPrompt
|
|
@@ -6036,24 +6202,24 @@ async function buildRouter4(deps) {
|
|
|
6036
6202
|
return Response.json(session, { status: 201 });
|
|
6037
6203
|
});
|
|
6038
6204
|
router.get("/sessions", async () => {
|
|
6039
|
-
const sessions2 = await listSessions(
|
|
6205
|
+
const sessions2 = await listSessions(sql4);
|
|
6040
6206
|
return Response.json(sessions2);
|
|
6041
6207
|
});
|
|
6042
6208
|
router.get("/sessions/:id", async (_req, ctx) => {
|
|
6043
6209
|
const id2 = ctx.params.id;
|
|
6044
|
-
const session = await getSession(
|
|
6210
|
+
const session = await getSession(sql4, id2);
|
|
6045
6211
|
if (!session) return new Response("Not Found", { status: 404 });
|
|
6046
|
-
const messages2 = await getHistory(
|
|
6212
|
+
const messages2 = await getHistory(sql4, id2);
|
|
6047
6213
|
return Response.json({ session, messages: messages2 });
|
|
6048
6214
|
});
|
|
6049
6215
|
router.delete("/sessions/:id", async (_req, ctx) => {
|
|
6050
6216
|
const id2 = ctx.params.id;
|
|
6051
|
-
await deleteSession(
|
|
6217
|
+
await deleteSession(sql4, id2);
|
|
6052
6218
|
return new Response(null, { status: 204 });
|
|
6053
6219
|
});
|
|
6054
6220
|
router.post("/sessions/:id/message", async (req, ctx) => {
|
|
6055
6221
|
const sessionId = ctx.params.id;
|
|
6056
|
-
const session = await getSession(
|
|
6222
|
+
const session = await getSession(sql4, sessionId);
|
|
6057
6223
|
if (!session) return new Response("Session Not Found", { status: 404 });
|
|
6058
6224
|
const { content } = await req.json();
|
|
6059
6225
|
if (!content) return new Response("Missing content", { status: 400 });
|
|
@@ -6071,8 +6237,8 @@ async function buildRouter4(deps) {
|
|
|
6071
6237
|
skills: allSkills,
|
|
6072
6238
|
systemPrompt: session.system_prompt || systemPrompt
|
|
6073
6239
|
});
|
|
6074
|
-
const history = await getHistory(
|
|
6075
|
-
await addTextMessage(
|
|
6240
|
+
const history = await getHistory(sql4, sessionId);
|
|
6241
|
+
await addTextMessage(sql4, sessionId, "user", content);
|
|
6076
6242
|
const stream = executeGenerator({
|
|
6077
6243
|
sessionId,
|
|
6078
6244
|
input: content,
|
|
@@ -6080,7 +6246,7 @@ async function buildRouter4(deps) {
|
|
|
6080
6246
|
tools,
|
|
6081
6247
|
systemPrompt: sysPrompt,
|
|
6082
6248
|
messages: history,
|
|
6083
|
-
sql:
|
|
6249
|
+
sql: sql4
|
|
6084
6250
|
});
|
|
6085
6251
|
return createSSEStream(stream);
|
|
6086
6252
|
});
|
|
@@ -6097,7 +6263,7 @@ async function buildRouter4(deps) {
|
|
|
6097
6263
|
// opencode/ws.ts
|
|
6098
6264
|
var clients = /* @__PURE__ */ new WeakMap();
|
|
6099
6265
|
function createWSHandler2(deps) {
|
|
6100
|
-
const { sql:
|
|
6266
|
+
const { sql: sql4, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
|
|
6101
6267
|
return {
|
|
6102
6268
|
open(ws, ctx) {
|
|
6103
6269
|
const userId = ctx.user?.id ?? 0;
|
|
@@ -6117,7 +6283,7 @@ function createWSHandler2(deps) {
|
|
|
6117
6283
|
switch (msg.type) {
|
|
6118
6284
|
case "create": {
|
|
6119
6285
|
try {
|
|
6120
|
-
const session = await createSession(
|
|
6286
|
+
const session = await createSession(sql4, {
|
|
6121
6287
|
userId: client.userId,
|
|
6122
6288
|
title: msg.title,
|
|
6123
6289
|
model: msg.model,
|
|
@@ -6140,7 +6306,7 @@ function createWSHandler2(deps) {
|
|
|
6140
6306
|
client.abortController = controller;
|
|
6141
6307
|
client.currentSessionId = session_id;
|
|
6142
6308
|
try {
|
|
6143
|
-
const session = await getSession(
|
|
6309
|
+
const session = await getSession(sql4, session_id);
|
|
6144
6310
|
if (!session) {
|
|
6145
6311
|
ws.send(JSON.stringify({ type: "error", error: "Session not found" }));
|
|
6146
6312
|
return;
|
|
@@ -6159,8 +6325,8 @@ function createWSHandler2(deps) {
|
|
|
6159
6325
|
skills: allSkills,
|
|
6160
6326
|
systemPrompt: session.system_prompt || systemPrompt
|
|
6161
6327
|
});
|
|
6162
|
-
const history = await getHistory(
|
|
6163
|
-
await addTextMessage(
|
|
6328
|
+
const history = await getHistory(sql4, session_id);
|
|
6329
|
+
await addTextMessage(sql4, session_id, "user", content);
|
|
6164
6330
|
const stream = executeGenerator({
|
|
6165
6331
|
sessionId: session_id,
|
|
6166
6332
|
input: content,
|
|
@@ -6168,7 +6334,7 @@ function createWSHandler2(deps) {
|
|
|
6168
6334
|
tools,
|
|
6169
6335
|
systemPrompt: sysPrompt,
|
|
6170
6336
|
messages: history,
|
|
6171
|
-
sql:
|
|
6337
|
+
sql: sql4,
|
|
6172
6338
|
abortSignal: controller.signal
|
|
6173
6339
|
});
|
|
6174
6340
|
for await (const event of stream) {
|
|
@@ -6303,7 +6469,7 @@ function buildSkillRegistry(discovered, manual) {
|
|
|
6303
6469
|
// opencode/client.ts
|
|
6304
6470
|
async function opencode(options) {
|
|
6305
6471
|
const pg = options.pg;
|
|
6306
|
-
const
|
|
6472
|
+
const sql4 = pg.sql;
|
|
6307
6473
|
const baseURL = options.baseURL || process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com/v1";
|
|
6308
6474
|
const apiKey = options.apiKey || process.env.DEEPSEEK_API_KEY;
|
|
6309
6475
|
const workspace = options.workspace || process.cwd();
|
|
@@ -6318,9 +6484,9 @@ async function opencode(options) {
|
|
|
6318
6484
|
const pendingQuestions = /* @__PURE__ */ new Map();
|
|
6319
6485
|
const base = new PgModule(pg);
|
|
6320
6486
|
return {
|
|
6321
|
-
migrate: () => migrate5(
|
|
6322
|
-
router: () => buildRouter4({ sql:
|
|
6323
|
-
wsHandler: () => createWSHandler2({ sql:
|
|
6487
|
+
migrate: () => migrate5(sql4),
|
|
6488
|
+
router: () => buildRouter4({ sql: sql4, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions }),
|
|
6489
|
+
wsHandler: () => createWSHandler2({ sql: sql4, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions }),
|
|
6324
6490
|
close: () => base.close()
|
|
6325
6491
|
};
|
|
6326
6492
|
}
|
|
@@ -6424,6 +6590,183 @@ function mailer(options) {
|
|
|
6424
6590
|
}
|
|
6425
6591
|
return { send, close };
|
|
6426
6592
|
}
|
|
6593
|
+
|
|
6594
|
+
// logdb/migrate.ts
|
|
6595
|
+
function pad(n) {
|
|
6596
|
+
return n < 10 ? "0" + n : String(n);
|
|
6597
|
+
}
|
|
6598
|
+
function startOfMonth(year, month) {
|
|
6599
|
+
return new Date(year, month, 1);
|
|
6600
|
+
}
|
|
6601
|
+
function formatYM(d) {
|
|
6602
|
+
return `${d.getFullYear()}_${pad(d.getMonth() + 1)}`;
|
|
6603
|
+
}
|
|
6604
|
+
function toISO(d) {
|
|
6605
|
+
return d.toISOString().slice(0, 19) + "+00:00";
|
|
6606
|
+
}
|
|
6607
|
+
async function ensurePartitions(sql4, tableName) {
|
|
6608
|
+
const now = /* @__PURE__ */ new Date();
|
|
6609
|
+
for (let i = 0; i < 13; i++) {
|
|
6610
|
+
const start = startOfMonth(now.getFullYear(), now.getMonth() + i);
|
|
6611
|
+
const end = startOfMonth(now.getFullYear(), now.getMonth() + i + 1);
|
|
6612
|
+
const partName = `${tableName}_${formatYM(start)}`;
|
|
6613
|
+
await sql4.unsafe(`
|
|
6614
|
+
CREATE TABLE IF NOT EXISTS "${partName}"
|
|
6615
|
+
PARTITION OF "${tableName}"
|
|
6616
|
+
FOR VALUES FROM ('${toISO(start)}') TO ('${toISO(end)}')
|
|
6617
|
+
`);
|
|
6618
|
+
}
|
|
6619
|
+
}
|
|
6620
|
+
async function migrate6(pg, tableName) {
|
|
6621
|
+
const entries = pgTable(tableName, {
|
|
6622
|
+
id: serial("id"),
|
|
6623
|
+
level: text("level").notNull(),
|
|
6624
|
+
source: text("source").notNull(),
|
|
6625
|
+
message: text("message").notNull(),
|
|
6626
|
+
metadata: jsonb("metadata").default(sql`'{}'::jsonb`),
|
|
6627
|
+
created_at: timestamptz("created_at").default(sql`NOW()`)
|
|
6628
|
+
});
|
|
6629
|
+
await entries.create(pg.sql, { partitionBy: partitionBy("range", "created_at") });
|
|
6630
|
+
await entries.createIndex(pg.sql, ["created_at", "id"]);
|
|
6631
|
+
await entries.createIndex(pg.sql, ["level"]);
|
|
6632
|
+
await entries.createIndex(pg.sql, ["source"]);
|
|
6633
|
+
await entries.createIndex(pg.sql, ["level", "created_at"]);
|
|
6634
|
+
await ensurePartitions(pg.sql, tableName);
|
|
6635
|
+
}
|
|
6636
|
+
|
|
6637
|
+
// logdb/rest.ts
|
|
6638
|
+
function createHandler(entries) {
|
|
6639
|
+
return async (req, ctx) => {
|
|
6640
|
+
const body = await req.json();
|
|
6641
|
+
if (!body.level || !body.source || !body.message) {
|
|
6642
|
+
return Response.json({ error: "level, source, message are required" }, { status: 400 });
|
|
6643
|
+
}
|
|
6644
|
+
const metadata = body.metadata ?? {};
|
|
6645
|
+
if (ctx.user) {
|
|
6646
|
+
metadata.user_id = ctx.user.id;
|
|
6647
|
+
}
|
|
6648
|
+
const row = await entries.insert({
|
|
6649
|
+
level: body.level,
|
|
6650
|
+
source: body.source,
|
|
6651
|
+
message: body.message,
|
|
6652
|
+
metadata
|
|
6653
|
+
});
|
|
6654
|
+
if (typeof row.metadata === "string") {
|
|
6655
|
+
try {
|
|
6656
|
+
row.metadata = JSON.parse(row.metadata);
|
|
6657
|
+
} catch {
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
6660
|
+
return Response.json(row, { status: 201 });
|
|
6661
|
+
};
|
|
6662
|
+
}
|
|
6663
|
+
function listHandler(entries) {
|
|
6664
|
+
return async (req) => {
|
|
6665
|
+
const url = new URL(req.url);
|
|
6666
|
+
const conditions = [];
|
|
6667
|
+
const level = url.searchParams.get("level");
|
|
6668
|
+
if (level) conditions.push(eq("level", level));
|
|
6669
|
+
const source = url.searchParams.get("source");
|
|
6670
|
+
if (source) conditions.push(eq("source", source));
|
|
6671
|
+
for (const [key, value] of url.searchParams) {
|
|
6672
|
+
if (key.startsWith("meta.")) {
|
|
6673
|
+
conditions.push(contains("metadata", { [key.slice(5)]: value }));
|
|
6674
|
+
}
|
|
6675
|
+
}
|
|
6676
|
+
const after = url.searchParams.get("after");
|
|
6677
|
+
if (after) conditions.push(gte("created_at", after));
|
|
6678
|
+
const before = url.searchParams.get("before");
|
|
6679
|
+
if (before) conditions.push(lt("created_at", before));
|
|
6680
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
|
|
6681
|
+
const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
|
|
6682
|
+
const { count, data } = await entries.readMany(
|
|
6683
|
+
conditions.length > 0 ? conditions : void 0,
|
|
6684
|
+
{ orderBy: { created_at: "desc" }, limit, offset }
|
|
6685
|
+
);
|
|
6686
|
+
for (const row of data) {
|
|
6687
|
+
if (typeof row.metadata === "string") {
|
|
6688
|
+
try {
|
|
6689
|
+
row.metadata = JSON.parse(row.metadata);
|
|
6690
|
+
} catch {
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6694
|
+
return Response.json({ entries: data, total: count });
|
|
6695
|
+
};
|
|
6696
|
+
}
|
|
6697
|
+
function getHandler(entries) {
|
|
6698
|
+
return async (_req, ctx) => {
|
|
6699
|
+
const id2 = ctx.params?.id;
|
|
6700
|
+
if (!id2) return Response.json({ error: "id is required" }, { status: 400 });
|
|
6701
|
+
const row = await entries.read(parseInt(id2));
|
|
6702
|
+
if (!row) return Response.json({ error: "not found" }, { status: 404 });
|
|
6703
|
+
if (typeof row.metadata === "string") {
|
|
6704
|
+
try {
|
|
6705
|
+
row.metadata = JSON.parse(row.metadata);
|
|
6706
|
+
} catch {
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
return Response.json(row);
|
|
6710
|
+
};
|
|
6711
|
+
}
|
|
6712
|
+
|
|
6713
|
+
// logdb/client.ts
|
|
6714
|
+
function logdb(options) {
|
|
6715
|
+
const pg = options.pg;
|
|
6716
|
+
const sql4 = pg.sql;
|
|
6717
|
+
const tableName = options.table ?? "_log_entries";
|
|
6718
|
+
const entries = pg.table(tableName, {
|
|
6719
|
+
id: serial("id"),
|
|
6720
|
+
level: text("level").notNull(),
|
|
6721
|
+
source: text("source").notNull(),
|
|
6722
|
+
message: text("message").notNull(),
|
|
6723
|
+
metadata: jsonb("metadata").default(sql`'{}'::jsonb`),
|
|
6724
|
+
created_at: timestamptz("created_at").default(sql`NOW()`)
|
|
6725
|
+
});
|
|
6726
|
+
async function log(input) {
|
|
6727
|
+
const row = await entries.insert({
|
|
6728
|
+
level: input.level,
|
|
6729
|
+
source: input.source,
|
|
6730
|
+
message: input.message,
|
|
6731
|
+
metadata: input.metadata ?? {}
|
|
6732
|
+
});
|
|
6733
|
+
return row;
|
|
6734
|
+
}
|
|
6735
|
+
function router() {
|
|
6736
|
+
const r = new Router();
|
|
6737
|
+
r.post("/", createHandler(entries));
|
|
6738
|
+
r.get("/", listHandler(entries));
|
|
6739
|
+
r.get("/:id", getHandler(entries));
|
|
6740
|
+
return r;
|
|
6741
|
+
}
|
|
6742
|
+
async function clean(retentionMonths) {
|
|
6743
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
6744
|
+
cutoff.setMonth(cutoff.getMonth() - retentionMonths);
|
|
6745
|
+
const partitions = await sql4.unsafe(`
|
|
6746
|
+
SELECT relid::regclass::text AS name
|
|
6747
|
+
FROM pg_partition_tree('"${tableName}"'::regclass)
|
|
6748
|
+
WHERE relid IS DISTINCT FROM '"${tableName}"'::regclass
|
|
6749
|
+
`);
|
|
6750
|
+
let dropped = 0;
|
|
6751
|
+
for (const { name } of partitions) {
|
|
6752
|
+
const match = name.match(/_(\d{4})_(\d{2})$/);
|
|
6753
|
+
if (!match) continue;
|
|
6754
|
+
const partDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1);
|
|
6755
|
+
if (partDate < cutoff) {
|
|
6756
|
+
await sql4.unsafe(`DROP TABLE IF EXISTS "${name}"`);
|
|
6757
|
+
dropped++;
|
|
6758
|
+
}
|
|
6759
|
+
}
|
|
6760
|
+
return dropped;
|
|
6761
|
+
}
|
|
6762
|
+
return {
|
|
6763
|
+
log,
|
|
6764
|
+
router,
|
|
6765
|
+
migrate: () => migrate6(pg, tableName),
|
|
6766
|
+
clean,
|
|
6767
|
+
close: () => pg.close()
|
|
6768
|
+
};
|
|
6769
|
+
}
|
|
6427
6770
|
export {
|
|
6428
6771
|
Router,
|
|
6429
6772
|
TsxContext,
|
|
@@ -6441,6 +6784,7 @@ export {
|
|
|
6441
6784
|
health,
|
|
6442
6785
|
i18n,
|
|
6443
6786
|
loadEnv,
|
|
6787
|
+
logdb,
|
|
6444
6788
|
logger,
|
|
6445
6789
|
mailer,
|
|
6446
6790
|
messager,
|