@sleep2agi/commhub-server 0.9.0-preview.68 → 0.9.0-preview.69
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.9.0-preview.
|
|
3
|
+
"version": "0.9.0-preview.69",
|
|
4
4
|
"description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/db.ts
CHANGED
|
@@ -964,7 +964,7 @@ db.exec(`
|
|
|
964
964
|
network_id TEXT NOT NULL,
|
|
965
965
|
title TEXT NOT NULL,
|
|
966
966
|
column_name TEXT NOT NULL DEFAULT 'pool' CHECK(column_name IN ('pool', 'doing', 'done')),
|
|
967
|
-
priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('high', 'normal', 'low')),
|
|
967
|
+
priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('high', 'normal', 'low', 'lowest')),
|
|
968
968
|
due_on TEXT,
|
|
969
969
|
assignee TEXT,
|
|
970
970
|
client_id TEXT,
|
|
@@ -372,7 +372,7 @@ describe("requirements stay on the hub", () => {
|
|
|
372
372
|
expect((await api(ownerToken, path, { method: "PATCH", body: JSON.stringify({ due: "" }) })).body.requirement.due).toBe("");
|
|
373
373
|
const listed = await api(ownerToken, "/api/requirements");
|
|
374
374
|
expect(listed.body.capabilities).toContain("due_datetime");
|
|
375
|
-
expect(listed.body.capabilities).toEqual(["agent_owner", "description", "checklist", "projects", "due_datetime", "external_ref", "archived", "agent_api", "sub_requirements", "tags"]);
|
|
375
|
+
expect(listed.body.capabilities).toEqual(["agent_owner", "description", "checklist", "projects", "due_datetime", "external_ref", "archived", "agent_api", "sub_requirements", "tags", "priority_lowest"]);
|
|
376
376
|
});
|
|
377
377
|
|
|
378
378
|
test("sub-requirements: parent_id in the same network, no cycles, ≤ 5 levels, child counts, filters, delete detaches children", async () => {
|
|
@@ -60,6 +60,13 @@ describe("requirements MCP tools", () => {
|
|
|
60
60
|
expect((await s.call("requirements_create", { name: "x", owner: { kind: "robot", id: "a" } })).mcpError).toBe(true);
|
|
61
61
|
expect((await s.call("requirements_checklist_toggle", { id: "r", item_id: "has space", done: true })).mcpError).toBe(true);
|
|
62
62
|
expect((await s.call("requirements_create", { name: "" })).mcpError).toBe(true);
|
|
63
|
+
// P0–P3:lowest(P3 极低)在建 / 改 / upsert 都能写;不认识的值在协议边界被拒
|
|
64
|
+
expect((await s.call("requirements_create", { name: "x", priority: "urgent" })).mcpError).toBe(true);
|
|
65
|
+
const low = await s.call("requirements_create", { name: "极低", priority: "lowest" });
|
|
66
|
+
expect(low.requirement.priority).toBe("lowest");
|
|
67
|
+
expect((await s.call("requirements_update", { id: low.requirement.id, priority: "high" })).requirement.priority).toBe("high");
|
|
68
|
+
expect((await s.call("requirements_update", { id: low.requirement.id, priority: "lowest" })).requirement.priority).toBe("lowest");
|
|
69
|
+
expect((await s.call("requirements_upsert_by_external_ref", { external_ref: "github:acme/p3#1", name: "同步", priority: "lowest" })).requirement.priority).toBe("lowest");
|
|
63
70
|
} finally { await s.close(); }
|
|
64
71
|
});
|
|
65
72
|
|
|
@@ -49,3 +49,35 @@ export function ensureRequirementIndexes(database: DbAdapter): void {
|
|
|
49
49
|
database.exec("CREATE INDEX IF NOT EXISTS idx_requirements_network_updated ON requirements(network_id, updated_at)");
|
|
50
50
|
database.exec("CREATE INDEX IF NOT EXISTS idx_requirements_parent ON requirements(parent_id) WHERE parent_id IS NOT NULL");
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 优先级加 lowest(P3 极低)。旧库建表时带 CHECK(priority IN ('high', 'normal', 'low')),SQLite 不能就地改
|
|
55
|
+
* CHECK,只能重建一次:拿 sqlite_master 里的原建表语句(含历次加的列)只换掉那个 CHECK,整表拷过去,
|
|
56
|
+
* 再按原样重建这张表的索引。已经放开的库什么都不做。存量值不改。
|
|
57
|
+
*/
|
|
58
|
+
const OLD_PRIORITY_CHECK = /CHECK\s*\(\s*priority\s+IN\s*\(\s*'high'\s*,\s*'normal'\s*,\s*'low'\s*\)\s*\)/i;
|
|
59
|
+
export const PRIORITY_CHECK = "CHECK(priority IN ('high', 'normal', 'low', 'lowest'))";
|
|
60
|
+
export function migrateRequirementPriorityCheck(database: DbAdapter): { rebuilt: boolean } {
|
|
61
|
+
if (database.dialect === "postgres") {
|
|
62
|
+
try {
|
|
63
|
+
database.exec("ALTER TABLE requirements DROP CONSTRAINT IF EXISTS requirements_priority_check");
|
|
64
|
+
database.exec(`ALTER TABLE requirements ADD CONSTRAINT requirements_priority_check ${PRIORITY_CHECK}`);
|
|
65
|
+
} catch {}
|
|
66
|
+
return { rebuilt: false };
|
|
67
|
+
}
|
|
68
|
+
const table = database.get<{ sql: string }>("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'requirements'");
|
|
69
|
+
if (!table?.sql || !OLD_PRIORITY_CHECK.test(table.sql)) return { rebuilt: false };
|
|
70
|
+
const indexes = database.all<{ sql: string }>("SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = 'requirements' AND sql IS NOT NULL");
|
|
71
|
+
const createSql = table.sql
|
|
72
|
+
.replace(/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?requirements["`]?/i, "CREATE TABLE requirements_migrated")
|
|
73
|
+
.replace(OLD_PRIORITY_CHECK, PRIORITY_CHECK);
|
|
74
|
+
database.transaction(() => {
|
|
75
|
+
database.exec("DROP TABLE IF EXISTS requirements_migrated");
|
|
76
|
+
database.exec(createSql);
|
|
77
|
+
database.exec("INSERT INTO requirements_migrated SELECT * FROM requirements");
|
|
78
|
+
database.exec("DROP TABLE requirements");
|
|
79
|
+
database.exec("ALTER TABLE requirements_migrated RENAME TO requirements");
|
|
80
|
+
for (const index of indexes) database.exec(index.sql);
|
|
81
|
+
});
|
|
82
|
+
return { rebuilt: true };
|
|
83
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// P0–P3 优先级:lowest(P3 极低)。生产库的 requirements 表建于旧 db.ts,带 CHECK(priority IN ('high', 'normal', 'low')),
|
|
2
|
+
// 光放开校验集合写进去也会被 SQLite 拒掉。这里先用旧建表语句建库、塞行,再走真实启动路径(db.ts + server.ts),
|
|
3
|
+
// 看:表重建一次、存量行 / 索引原样、lowest 在 POST / PATCH / upsert 都能写、坏值仍被拒、再启动不再重建。
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "anet-req-priority-"));
|
|
11
|
+
const file = join(dir, "hub.db");
|
|
12
|
+
process.env.COMMHUB_DB = file;
|
|
13
|
+
|
|
14
|
+
const legacy = new Database(file);
|
|
15
|
+
legacy.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS requirements (
|
|
17
|
+
requirement_id TEXT PRIMARY KEY,
|
|
18
|
+
network_id TEXT NOT NULL,
|
|
19
|
+
title TEXT NOT NULL,
|
|
20
|
+
column_name TEXT NOT NULL DEFAULT 'pool' CHECK(column_name IN ('pool', 'doing', 'done')),
|
|
21
|
+
priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('high', 'normal', 'low')),
|
|
22
|
+
due_on TEXT,
|
|
23
|
+
assignee TEXT,
|
|
24
|
+
client_id TEXT,
|
|
25
|
+
issues_json TEXT,
|
|
26
|
+
created_by TEXT,
|
|
27
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
28
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
29
|
+
);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_requirements_network ON requirements(network_id, created_at DESC);
|
|
31
|
+
`);
|
|
32
|
+
legacy.exec("ALTER TABLE requirements ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]'");
|
|
33
|
+
legacy.exec("ALTER TABLE requirements ADD COLUMN description TEXT");
|
|
34
|
+
const seeded = [["r_high", "high"], ["r_normal", "normal"], ["r_low", "low"]] as const;
|
|
35
|
+
for (const [id, p] of seeded) {
|
|
36
|
+
legacy.run("INSERT INTO requirements (requirement_id, network_id, title, priority, client_id, description, tags_json) VALUES (?1, 'net_legacy', ?2, ?3, ?4, ?5, '[\"旧\"]')", [id, `卡 ${id}`, p, `c_${id}`, `desc ${id}`]);
|
|
37
|
+
}
|
|
38
|
+
// 旧库确实拒 lowest:这就是光改 PRIORITIES 不够的原因
|
|
39
|
+
expect(() => legacy.run("INSERT INTO requirements (requirement_id, network_id, title, priority) VALUES ('x', 'n', 't', 'lowest')")).toThrow(/CHECK/);
|
|
40
|
+
legacy.close();
|
|
41
|
+
|
|
42
|
+
let base = "";
|
|
43
|
+
let token = "";
|
|
44
|
+
let server: { port: number; stop?: (force?: boolean) => void };
|
|
45
|
+
let db: any;
|
|
46
|
+
|
|
47
|
+
async function api(path: string, init?: RequestInit) {
|
|
48
|
+
const res = await fetch(`${base}${path}`, { ...init, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } });
|
|
49
|
+
return { status: res.status, body: await res.json() as any };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeAll(async () => {
|
|
53
|
+
({ db } = await import("./db.js"));
|
|
54
|
+
const { register } = await import("./auth.js");
|
|
55
|
+
const owner = register(`prio_owner_${Date.now()}`, "PriorityOwner123!", undefined, "seed");
|
|
56
|
+
token = owner.token!;
|
|
57
|
+
const mod: any = await import("./server.js");
|
|
58
|
+
server = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
59
|
+
base = `http://127.0.0.1:${server.port}`;
|
|
60
|
+
}, 30_000);
|
|
61
|
+
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
try { server?.stop?.(true); } catch {}
|
|
64
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("P3 lowest priority", () => {
|
|
68
|
+
test("startup rebuilds the legacy CHECK once and keeps every row and index", async () => {
|
|
69
|
+
const sql = db.get("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'requirements'").sql as string;
|
|
70
|
+
expect(sql).toContain("'lowest'");
|
|
71
|
+
expect(sql).toContain("CHECK(column_name IN ('pool', 'doing', 'done'))");
|
|
72
|
+
const rows = db.all("SELECT requirement_id, network_id, title, priority, client_id, description, tags_json FROM requirements WHERE network_id = 'net_legacy' ORDER BY requirement_id") as any[];
|
|
73
|
+
expect(rows.map(r => [r.requirement_id, r.priority])).toEqual([["r_high", "high"], ["r_low", "low"], ["r_normal", "normal"]]);
|
|
74
|
+
for (const r of rows) {
|
|
75
|
+
expect(r.title).toBe(`卡 ${r.requirement_id}`);
|
|
76
|
+
expect(r.client_id).toBe(`c_${r.requirement_id}`);
|
|
77
|
+
expect(r.description).toBe(`desc ${r.requirement_id}`);
|
|
78
|
+
expect(r.tags_json).toBe('["旧"]');
|
|
79
|
+
}
|
|
80
|
+
const indexes = (db.all("PRAGMA index_list(requirements)") as { name: string; unique: number }[]).map(i => i.name);
|
|
81
|
+
for (const name of ["idx_requirements_network", "idx_requirements_client", "idx_requirements_external_ref", "idx_requirements_network_updated", "idx_requirements_parent"]) expect(indexes).toContain(name);
|
|
82
|
+
expect(db.get("SELECT name FROM sqlite_master WHERE name = 'requirements_migrated'")).toBeNull();
|
|
83
|
+
// 坏值仍被数据库拒
|
|
84
|
+
expect(() => db.run("INSERT INTO requirements (requirement_id, network_id, title, priority) VALUES ('bad', 'n', 't', 'urgent')")).toThrow(/CHECK/);
|
|
85
|
+
// 下次启动:已经放开,不再重建
|
|
86
|
+
const { migrateRequirementPriorityCheck } = await import("./requirements-migrate.js");
|
|
87
|
+
expect(migrateRequirementPriorityCheck(db).rebuilt).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("lowest is accepted on create, PATCH and upsert; unknown values stay 400", async () => {
|
|
91
|
+
const created = await api("/api/requirements", { method: "POST", body: JSON.stringify({ name: "极低的活", priority: "lowest" }) });
|
|
92
|
+
expect(created.status).toBe(201);
|
|
93
|
+
expect(created.body.requirement.priority).toBe("lowest");
|
|
94
|
+
const id = created.body.requirement.id;
|
|
95
|
+
const patched = await api(`/api/requirements/${id}`, { method: "PATCH", body: JSON.stringify({ priority: "low" }) });
|
|
96
|
+
expect(patched.body.requirement.priority).toBe("low");
|
|
97
|
+
const back = await api(`/api/requirements/${id}`, { method: "PATCH", body: JSON.stringify({ priority: "lowest" }) });
|
|
98
|
+
expect(back.status).toBe(200);
|
|
99
|
+
expect(back.body.requirement.priority).toBe("lowest");
|
|
100
|
+
const up = await api("/api/requirements/upsert", { method: "POST", body: JSON.stringify({ name: "同步来的", external_ref: "github:t/p#1", priority: "lowest" }) });
|
|
101
|
+
expect(up.status).toBe(201);
|
|
102
|
+
expect(up.body.requirement.priority).toBe("lowest");
|
|
103
|
+
const upAgain = await api("/api/requirements/upsert", { method: "POST", body: JSON.stringify({ external_ref: "github:t/p#1", priority: "high" }) });
|
|
104
|
+
expect(upAgain.body.requirement.priority).toBe("high");
|
|
105
|
+
for (const [path, method] of [["/api/requirements", "POST"], [`/api/requirements/${id}`, "PATCH"]] as const) {
|
|
106
|
+
const bad = await api(path, { method, body: JSON.stringify({ name: "x", priority: "urgent" }) });
|
|
107
|
+
expect(bad.status).toBe(400);
|
|
108
|
+
expect(bad.body.error).toBe("invalid_priority");
|
|
109
|
+
}
|
|
110
|
+
const listed = await api("/api/requirements");
|
|
111
|
+
expect(listed.body.capabilities).toContain("priority_lowest");
|
|
112
|
+
expect(listed.body.requirements.find((r: any) => r.id === id).priority).toBe("lowest");
|
|
113
|
+
});
|
|
114
|
+
});
|
package/src/requirements.ts
CHANGED
|
@@ -4,10 +4,11 @@ import { db } from "./db.js";
|
|
|
4
4
|
import { normalizeTags, storedTags } from "./requirement-tags.js";
|
|
5
5
|
import { addHumanNetworkScope, canRestWriteNetwork, canRestWriteNetworkAsHuman, resolveRestWriteNetworkId, type RestNetworkScope } from "./network-scope.js";
|
|
6
6
|
import { isAgentRestricted, visibleAgents } from "./agent-access.js";
|
|
7
|
-
import { ensureRequirementIndexes, ensureRequirementProjects, migrateRequirementAgentOwners } from "./requirements-migrate.js";
|
|
7
|
+
import { ensureRequirementIndexes, ensureRequirementProjects, migrateRequirementAgentOwners, migrateRequirementPriorityCheck } from "./requirements-migrate.js";
|
|
8
8
|
|
|
9
9
|
// 启动迁移:旧库里节点当负责人的卡,节点挪到 agent_owner(列由 db.ts 的加列循环加上)。
|
|
10
10
|
// 放在这里而不是 db.ts:db.ts 每多一行,文档里钉着的行号就漂一次。
|
|
11
|
+
migrateRequirementPriorityCheck(db);
|
|
11
12
|
migrateRequirementAgentOwners(db);
|
|
12
13
|
ensureRequirementProjects(db);
|
|
13
14
|
ensureRequirementIndexes(db);
|
|
@@ -24,7 +25,8 @@ export type RequirementsRequestContext = {
|
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
const COLUMNS = new Set(["pool", "doing", "done"]);
|
|
27
|
-
|
|
28
|
+
// 界面上 P0–P3:high=P0 最高、normal=P1 普通、low=P2 低、lowest=P3 极低。存的值不变,旧客户端照常读写前三个。
|
|
29
|
+
const PRIORITIES = new Set(["high", "normal", "low", "lowest"]);
|
|
28
30
|
const DUE = /^\d{4}-\d{2}-\d{2}$/;
|
|
29
31
|
|
|
30
32
|
type Row = {
|
|
@@ -425,7 +427,7 @@ function operationOf(req: Request, url: URL): string {
|
|
|
425
427
|
return "create";
|
|
426
428
|
}
|
|
427
429
|
|
|
428
|
-
export const REQUIREMENT_CAPABILITIES = ["agent_owner", "description", "checklist", "projects", "due_datetime", "external_ref", "archived", "agent_api", "sub_requirements", "tags"] as const;
|
|
430
|
+
export const REQUIREMENT_CAPABILITIES = ["agent_owner", "description", "checklist", "projects", "due_datetime", "external_ref", "archived", "agent_api", "sub_requirements", "tags", "priority_lowest"] as const;
|
|
429
431
|
|
|
430
432
|
// ── 子需求 ──
|
|
431
433
|
// parent_id:同一网络里的另一张卡;不能成环;最多 5 层(顶层是第 1 层)。删父卡 = 子卡保留、parent_id 置空。
|
package/src/tools.ts
CHANGED
|
@@ -5765,7 +5765,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5765
5765
|
const reqFields = {
|
|
5766
5766
|
tags: z.array(z.string()).max(10).optional().describe("Task labels; each 1–20 Unicode characters. Omit to preserve, [] to clear."),
|
|
5767
5767
|
name: z.string().min(1).max(80).optional(),
|
|
5768
|
-
priority: z.enum(["high", "normal", "low"]).optional(),
|
|
5768
|
+
priority: z.enum(["high", "normal", "low", "lowest"]).optional().describe("high = P0 最高, normal = P1 普通, low = P2 低, lowest = P3 极低"),
|
|
5769
5769
|
column: z.enum(["pool", "doing", "done"]).optional(),
|
|
5770
5770
|
due: z.string().max(40).optional().describe("YYYY-MM-DD (all day) or ISO 8601 with Z / ±HH:MM (stored as UTC seconds); \"\" clears"),
|
|
5771
5771
|
description: z.string().max(20_000).optional().describe("markdown"),
|