@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.68",
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
+ });
@@ -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
- const PRIORITIES = new Set(["high", "normal", "low"]);
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"),