@co0ontty/wand 4.89.1 → 4.89.2-beta.gd071c20

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/dist/storage.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { FIXED_EMPLOYEE_AVATARS, fixedEmployeeAvatar, isFixedAvatarEmployee } from "./fixed-employee-avatar.js";
2
3
  import { CONVERSATION_OWNER } from "./conversation-types.js";
3
4
  import { chmodSync, existsSync, mkdirSync } from "node:fs";
4
5
  import path from "node:path";
@@ -2719,9 +2720,14 @@ export class WandStorage {
2719
2720
  const state = this.getUserMemoryState();
2720
2721
  return defaultEmployeeDefinition(employee.agents, employee.updatedAt, employee, state.enabled ? state.profile : null);
2721
2722
  }
2722
- getSiliconEmployee(id) {
2723
+ /** Saved definition without generated short-term preferences. */
2724
+ getSiliconEmployeeDefinition(id) {
2723
2725
  const row = this.db.prepare("SELECT * FROM silicon_employees WHERE id = ?").get(id);
2724
- return row ? this.projectDefaultEmployee(mapSiliconEmployeeRow(row)) : null;
2726
+ return row ? mapSiliconEmployeeRow(row) : null;
2727
+ }
2728
+ getSiliconEmployee(id) {
2729
+ const employee = this.getSiliconEmployeeDefinition(id);
2730
+ return employee ? this.projectDefaultEmployee(employee) : null;
2725
2731
  }
2726
2732
  getDefaultSiliconEmployee() {
2727
2733
  const employee = this.getSystemSiliconEmployee(DEFAULT_EMPLOYEE_KEY);
@@ -2730,43 +2736,25 @@ export class WandStorage {
2730
2736
  /** Seed once, preserve execution choices, and never reset learned preferences on restart. */
2731
2737
  ensureDefaultSiliconEmployee(provider) {
2732
2738
  const existing = this.getSystemSiliconEmployee(DEFAULT_EMPLOYEE_KEY);
2733
- const state = this.getUserMemoryState();
2734
- const definition = defaultEmployeeDefinition(existing?.agents ?? systemEmployeeSeedAgents({ provider }), new Date().toISOString(), existing, state.enabled ? state.profile : null);
2735
- if (!existing || existing.name !== definition.name || existing.duty !== definition.duty
2736
- || existing.prompt !== definition.prompt || existing.avatar !== definition.avatar
2737
- || existing.archivedAt || !existing.agents.length) {
2738
- this.saveSiliconEmployee(definition);
2739
- return definition;
2740
- }
2741
- return existing;
2739
+ if (existing)
2740
+ return this.projectDefaultEmployee(existing);
2741
+ const definition = defaultEmployeeDefinition(systemEmployeeSeedAgents({ provider }), new Date().toISOString());
2742
+ this.saveSiliconEmployee(definition);
2743
+ return this.projectDefaultEmployee(definition);
2742
2744
  }
2743
2745
  /** 内置「系统运维」员工;未创建(或被删除过)时返回 null。 */
2744
2746
  getSystemSiliconEmployee(key = SYSTEM_EMPLOYEE_KEY) {
2745
2747
  const row = this.db.prepare("SELECT * FROM silicon_employees WHERE system_key = ?").get(key);
2746
2748
  return row ? mapSiliconEmployeeRow(row) : null;
2747
2749
  }
2748
- /**
2749
- * 幂等地保证内置员工存在。已存在时只补齐锁定字段(防止旧的同名定义漂移),
2750
- * 候选保留用户当下的设置;不存在时按 seed 建首条候选。
2751
- */
2750
+ /** Seed only when missing; restart never overwrites user profile, candidates, or legacy avatars. */
2752
2751
  ensureSystemSiliconEmployee(seed = {}) {
2753
2752
  const existing = this.getSystemSiliconEmployee();
2754
- if (!existing) {
2755
- const employee = systemEmployeeDefinition(systemEmployeeSeedAgents(seed), new Date().toISOString());
2756
- this.saveSiliconEmployee(employee);
2757
- return employee;
2758
- }
2759
- const locked = systemEmployeeDefinition(existing.agents, new Date().toISOString(), existing);
2760
- if (existing.name !== locked.name
2761
- || existing.duty !== locked.duty
2762
- || existing.prompt !== locked.prompt
2763
- || existing.avatar !== locked.avatar
2764
- || existing.archivedAt !== undefined
2765
- || existing.agents.length === 0) {
2766
- this.saveSiliconEmployee(locked);
2767
- return locked;
2768
- }
2769
- return existing;
2753
+ if (existing)
2754
+ return existing;
2755
+ const employee = systemEmployeeDefinition(systemEmployeeSeedAgents(seed), new Date().toISOString());
2756
+ this.saveSiliconEmployee(employee);
2757
+ return employee;
2770
2758
  }
2771
2759
  /** Stable built-in decision employee; seed once and retain the user's ordered call chain. */
2772
2760
  ensureDecisionExpertEmployee() {
@@ -2791,6 +2779,20 @@ export class WandStorage {
2791
2779
  return existing;
2792
2780
  }
2793
2781
  saveSiliconEmployee(employee) {
2782
+ const existing = this.getSiliconEmployeeDefinition(employee.id);
2783
+ const target = FIXED_EMPLOYEE_AVATARS.find((entry) => entry.id === employee.id || entry.systemKey === employee.systemKey);
2784
+ if (target) {
2785
+ if (employee.id !== target.id || employee.systemKey !== target.systemKey)
2786
+ throw new Error("内置员工身份不可修改或冒用。");
2787
+ if (existing && !isFixedAvatarEmployee(existing))
2788
+ throw new Error("内置员工身份冲突,不能覆盖已有记录。");
2789
+ if (employee.avatar !== (existing?.avatar ?? target.avatar))
2790
+ throw new Error("该员工是内置的,头像不可修改。");
2791
+ if (existing && JSON.stringify(employee.tags) !== JSON.stringify(existing.tags))
2792
+ throw new Error("该员工是内置的,标签不可修改。");
2793
+ if (employee.archivedAt !== existing?.archivedAt)
2794
+ throw new Error("内置员工不可归档。");
2795
+ }
2794
2796
  const tags = isBuiltinSiliconEmployee(employee)
2795
2797
  ? siliconEmployeeTags(employee)
2796
2798
  : parseSiliconEmployeeTags(employee.tags ?? []);
@@ -2808,13 +2810,46 @@ export class WandStorage {
2808
2810
  archived_at = excluded.archived_at,
2809
2811
  updated_at = excluded.updated_at`).run(employee.id, employee.name, employee.duty, employee.prompt, employee.avatar, JSON.stringify(employee.agents), employee.systemKey ?? null, employee.archivedAt ?? null, employee.createdAt, employee.updatedAt, JSON.stringify(tags));
2810
2812
  }
2813
+ /** Explicit, atomic migration; not called from GET, config load, seeds, or ordinary saves.
2814
+ * Compare all expected avatar values first; change only avatar, including preserving timestamps.
2815
+ */
2816
+ migrateFixedEmployeeAvatars(expected) {
2817
+ return this.transaction(() => {
2818
+ if (expected.length !== FIXED_EMPLOYEE_AVATARS.length || new Set(expected.map((entry) => entry.id)).size !== expected.length) {
2819
+ throw new Error("固定头像迁移需要两个精确且不同的目标。");
2820
+ }
2821
+ const rows = FIXED_EMPLOYEE_AVATARS.map((target) => {
2822
+ const prior = expected.find((entry) => entry.id === target.id && entry.systemKey === target.systemKey);
2823
+ const employee = this.getSiliconEmployeeDefinition(target.id);
2824
+ if (!prior || !employee || fixedEmployeeAvatar(employee) !== target.avatar)
2825
+ throw new Error("固定头像迁移目标不存在或身份冲突。");
2826
+ if (employee.avatar !== prior.avatar && employee.avatar !== target.avatar)
2827
+ throw new Error("固定头像迁移检测到头像已变化,请重新核对。");
2828
+ return { target, employee };
2829
+ });
2830
+ let changed = 0;
2831
+ for (const { target, employee } of rows) {
2832
+ if (employee.avatar === target.avatar)
2833
+ continue;
2834
+ changed += Number(this.db.prepare("UPDATE silicon_employees SET avatar = ? WHERE id = ? AND system_key = ? AND avatar = ?")
2835
+ .run(target.avatar, target.id, target.systemKey, employee.avatar).changes);
2836
+ }
2837
+ return changed;
2838
+ });
2839
+ }
2811
2840
  archiveSiliconEmployee(id, archivedAt = new Date().toISOString()) {
2841
+ if (isFixedAvatarEmployee(this.getSiliconEmployeeDefinition(id) ?? {}))
2842
+ throw new Error("内置员工不可归档。");
2812
2843
  this.db.prepare("UPDATE silicon_employees SET archived_at = ?, updated_at = ? WHERE id = ?").run(archivedAt, new Date().toISOString(), id);
2813
2844
  }
2814
2845
  unarchiveSiliconEmployee(id) {
2846
+ if (isFixedAvatarEmployee(this.getSiliconEmployeeDefinition(id) ?? {}))
2847
+ throw new Error("内置员工不能恢复归档。");
2815
2848
  this.db.prepare("UPDATE silicon_employees SET archived_at = NULL, updated_at = ? WHERE id = ?").run(new Date().toISOString(), id);
2816
2849
  }
2817
2850
  deleteSiliconEmployee(id) {
2851
+ if (isFixedAvatarEmployee(this.getSiliconEmployeeDefinition(id) ?? {}))
2852
+ throw new Error("内置员工不可删除。");
2818
2853
  this.db.prepare("DELETE FROM silicon_employees WHERE id = ?").run(id);
2819
2854
  }
2820
2855
  // ============ Employee-owned explicit knowledge ============
@@ -6,8 +6,8 @@ export { SYSTEM_EMPLOYEE_ID, SYSTEM_EMPLOYEE_KEY, SYSTEM_EMPLOYEE_NAME, SYSTEM_E
6
6
  * 内置的「系统运维」员工。
7
7
  *
8
8
  * Wand 自己的 AI 调用(commit message / tag、提示词优化、会话与任务标题、
9
- * 员工起草)都通过这位员工的候选链执行:名字、职责与人设写死在代码里,不可编辑、不可归档、
10
- * 不可删除;只有执行候选(工具 + 引擎 + 模型 + 思考深度,按顺序降级)由用户维护。
9
+ * 员工起草)都通过这位员工的候选链执行:身份和头像固定,不可归档、不可删除;
10
+ * 名字、职责、人设与执行候选(工具 + 引擎 + 模型 + 思考深度)由用户维护。
11
11
  */
12
12
  export declare const SYSTEM_EMPLOYEE_DUTY = "Wand \u7CFB\u7EDF\u8FD0\u7EF4\uFF1A\u670D\u52A1\u4E0E CLI \u7EBF\u8DEF\u3001\u66F4\u65B0\u5206\u53D1\u3001\u4ED3\u5E93\u4E0E\u4F1A\u8BDD\u64CD\u4F5C\uFF0CWand \u81EA\u6709 AI \u4EFB\u52A1\u7684\u6267\u884C\u8005\u3002";
13
13
  /**
@@ -1,4 +1,5 @@
1
1
  import { SYSTEM_EMPLOYEE_ID, SYSTEM_EMPLOYEE_KEY, SYSTEM_EMPLOYEE_NAME, SYSTEM_EMPLOYEE_TAG, isSystemSiliconEmployee, } from "./ai-team-types.js";
2
+ import { fixedEmployeeAvatar } from "./fixed-employee-avatar.js";
2
3
  import { isSessionProvider } from "./session-provider.js";
3
4
  import { DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_AGENT_MODE, normalizeWandTaskAgentMode } from "./task-types.js";
4
5
  // 常量与判定定义在 ai-team-types(浏览器也要用),这里再导出一次,服务端只从一个地方引用。
@@ -7,8 +8,8 @@ export { SYSTEM_EMPLOYEE_ID, SYSTEM_EMPLOYEE_KEY, SYSTEM_EMPLOYEE_NAME, SYSTEM_E
7
8
  * 内置的「系统运维」员工。
8
9
  *
9
10
  * Wand 自己的 AI 调用(commit message / tag、提示词优化、会话与任务标题、
10
- * 员工起草)都通过这位员工的候选链执行:名字、职责与人设写死在代码里,不可编辑、不可归档、
11
- * 不可删除;只有执行候选(工具 + 引擎 + 模型 + 思考深度,按顺序降级)由用户维护。
11
+ * 员工起草)都通过这位员工的候选链执行:身份和头像固定,不可归档、不可删除;
12
+ * 名字、职责、人设与执行候选(工具 + 引擎 + 模型 + 思考深度)由用户维护。
12
13
  */
13
14
  export const SYSTEM_EMPLOYEE_DUTY = "Wand 系统运维:服务与 CLI 线路、更新分发、仓库与会话操作,Wand 自有 AI 任务的执行者。";
14
15
  /**
@@ -45,11 +46,10 @@ export function systemEmployeeSeedAgents(seed = {}) {
45
46
  export function systemEmployeeDefinition(agents, now, existing) {
46
47
  return {
47
48
  id: existing?.id ?? SYSTEM_EMPLOYEE_ID,
48
- name: SYSTEM_EMPLOYEE_NAME,
49
- duty: SYSTEM_EMPLOYEE_DUTY,
50
- prompt: SYSTEM_EMPLOYEE_PROMPT,
51
- // 空头像 = 按 id/name 稳定派生一只像素猫,与用户员工同一套。
52
- avatar: "",
49
+ name: existing?.name ?? SYSTEM_EMPLOYEE_NAME,
50
+ duty: existing?.duty ?? SYSTEM_EMPLOYEE_DUTY,
51
+ prompt: existing?.prompt ?? SYSTEM_EMPLOYEE_PROMPT,
52
+ avatar: existing?.avatar ?? fixedEmployeeAvatar({ id: SYSTEM_EMPLOYEE_ID, systemKey: SYSTEM_EMPLOYEE_KEY }),
53
53
  agents: agents.length ? agents : systemEmployeeSeedAgents(),
54
54
  systemKey: SYSTEM_EMPLOYEE_KEY,
55
55
  tags: [SYSTEM_EMPLOYEE_TAG],
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { isSessionProvider } from "./session-provider.js";
2
3
  import { provisionalTaskTitleFromDescription } from "./task-title.js";
3
4
  /** 未命名任务的占位名;服务端新建时写入,自动命名成功后被真实标题覆盖。 */
4
5
  export const UNNAMED_WORKSPACE_TASK_NAME = "未命名任务";
@@ -10,6 +11,8 @@ export function isUnnamedWorkspaceTaskName(name) {
10
11
  }
11
12
  /** Legacy title helper; prompts name sessions, not their containing task. */
12
13
  export function boardTitleFromSession(session) {
14
+ if (isBlankTerminal(session))
15
+ return "";
13
16
  const title = session.title?.trim();
14
17
  if (title && !isUnnamedWorkspaceTaskName(title))
15
18
  return provisionalTaskTitleFromDescription(title);
@@ -26,6 +29,9 @@ export function boardTitleFromSession(session) {
26
29
  }
27
30
  return "";
28
31
  }
32
+ function isBlankTerminal(session) {
33
+ return session.sessionKind !== "structured" && !isSessionProvider(session.provider);
34
+ }
29
35
  /**
30
36
  * 自动命名只覆盖「还没有人为名字」的任务:标题来源是 auto,或仍是占位名的历史卡片
31
37
  * (老版本把未命名任务写成了 titleSource=user)。用户在面板 / 侧栏改过名后此处为 false。
@@ -46,8 +52,9 @@ function taskNamingSessions(storage, card) {
46
52
  return;
47
53
  add(session);
48
54
  // boardTitleFromSession 只在 title/description 都缺时才翻 messages 找首条用户输入。
49
- if (!session.title?.trim() && !session.description?.trim())
55
+ if (!isBlankTerminal(session) && !session.title?.trim() && !session.description?.trim()) {
50
56
  needsMessageFallback.push(session.id);
57
+ }
51
58
  };
52
59
  if (card.workspaceTaskId) {
53
60
  for (const session of storage.listSessionsByWorkspaceTaskSlim(card.workspaceTaskId))
@@ -68,6 +75,10 @@ function taskNamingSessions(storage, card) {
68
75
  * 为空表示这个任务还没有可命名的内容,调用方应保留占位标题。
69
76
  */
70
77
  export function taskAutoNameSourceText(storage, card) {
78
+ const sessions = taskNamingSessions(storage, card);
79
+ // 只有空白终端的任务不总结;后来加入 Agent 时再按真实任务内容命名。
80
+ if (sessions.length > 0 && sessions.every(isBlankTerminal))
81
+ return "";
71
82
  const parts = [];
72
83
  for (const line of card.description.split(/\r?\n/)) {
73
84
  const trimmed = line.trim();
@@ -75,7 +86,7 @@ export function taskAutoNameSourceText(storage, card) {
75
86
  continue;
76
87
  parts.push(trimmed);
77
88
  }
78
- for (const session of taskNamingSessions(storage, card)) {
89
+ for (const session of sessions) {
79
90
  const candidate = boardTitleFromSession(session);
80
91
  if (candidate)
81
92
  parts.push(candidate);