@myclaw163/clawclaw-cli 0.6.67 → 0.6.68

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.
Files changed (40) hide show
  1. package/bin/clawclaw-cli.mjs +3 -3
  2. package/package.json +1 -1
  3. package/scripts/sync-bundled-skill.mjs +1 -1
  4. package/skills/clawclaw/references/COMMANDS.md +4 -4
  5. package/skills/clawclaw/references/KNOWLEDGE.md +14 -12
  6. package/src/commands/config.ts +30 -30
  7. package/src/commands/knowledge.test.ts +4 -10
  8. package/src/commands/knowledge.ts +10 -39
  9. package/src/commands/setup/hermes.test.ts +96 -96
  10. package/src/commands/setup/hermes.ts +76 -76
  11. package/src/commands/setup/index.ts +13 -13
  12. package/src/commands/setup/openclaw.test.ts +114 -114
  13. package/src/commands/setup/openclaw.ts +147 -147
  14. package/src/lib/host-config-patcher.test.ts +130 -130
  15. package/src/lib/host-config-patcher.ts +151 -151
  16. package/src/lib/hub-reminder.ts +19 -19
  17. package/src/lib/knowledge-store.test.ts +28 -38
  18. package/src/lib/knowledge-store.ts +52 -57
  19. package/src/sdk/index.ts +2 -3
  20. package/src/sdk/types.ts +2 -0
  21. package/src/strategies/avoid-players.knowledge.md +7 -8
  22. package/src/strategies/avoid-players.ts +1 -1
  23. package/src/strategies/corpse-patrol.ts +1 -1
  24. package/src/strategies/game-utils.ts +29 -17
  25. package/src/strategies/goals/avoid-players-top.ts +3 -3
  26. package/src/strategies/goals/corpse-patrol-top.ts +23 -1
  27. package/src/strategies/goals/leaf-goal.ts +2 -0
  28. package/src/strategies/goals/lone-kill-task-top.ts +39 -8
  29. package/src/strategies/goals/normal-shrimp-top.ts +11 -11
  30. package/src/strategies/goals/paradise-fish-top.ts +32 -15
  31. package/src/strategies/goals/warrior-shrimp-top.test.ts +4 -3
  32. package/src/strategies/goals/warrior-shrimp-top.ts +140 -80
  33. package/src/strategies/kill-lone.knowledge.md +6 -9
  34. package/src/strategies/lone-kill-task.ts +1 -1
  35. package/src/strategies/paradise-fish.knowledge.md +7 -8
  36. package/src/strategies/paradise-fish.ts +1 -1
  37. package/src/strategies/shrimp-memory.knowledge.md +7 -8
  38. package/src/strategies/shrimp-memory.ts +1 -1
  39. package/src/strategies/warrior-memory.knowledge.md +9 -10
  40. package/src/strategies/warrior-memory.ts +1 -1
@@ -46,8 +46,7 @@ describe('KnowledgeStore', () => {
46
46
  selector: '5',
47
47
  key: 'role',
48
48
  value: 'impostor',
49
- tags: ['suspect', 'kill_if_armed'],
50
- confidence: 0.8,
49
+ tags: ['kill_if_armed'],
51
50
  note: 'vent read',
52
51
  });
53
52
 
@@ -55,9 +54,7 @@ describe('KnowledgeStore', () => {
55
54
 
56
55
  expect(file?.gameId).toBe('game-1');
57
56
  expect(file?.subjects['player:5']?.facts?.role.value).toBe('impostor');
58
- expect(file?.subjects['player:5']?.facts?.role.confidence).toBe(0.8);
59
- expect(file?.subjects['player:5']?.tags?.suspect.note).toBe('vent read');
60
- expect(file?.subjects['player:5']?.tags?.kill_if_armed.confidence).toBe(0.8);
57
+ expect(file?.subjects['player:5']?.tags?.kill_if_armed.note).toBe('vent read');
61
58
  });
62
59
 
63
60
  it('preserves explicit null fact values', () => {
@@ -79,16 +76,8 @@ describe('KnowledgeStore', () => {
79
76
  expect(readKnowledgeFileResult(path).status).toBe('invalid');
80
77
  });
81
78
 
82
- it('rejects invalid confidence values', () => {
83
- const s = store();
84
-
85
- expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: Number.NaN })).toThrow(/confidence/);
86
- expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: -0.1 })).toThrow(/confidence/);
87
- expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: 1.1 })).toThrow(/confidence/);
88
- });
89
-
90
79
  it('returns an empty file when loading knowledge from another game', () => {
91
- store('old-game').set({ type: 'player', selector: '5', tags: ['avoid'] });
80
+ store('old-game').set({ type: 'player', selector: '5', tags: ['hostile'] });
92
81
 
93
82
  expect(store('new-game').list()).toEqual({
94
83
  version: 1,
@@ -97,14 +86,30 @@ describe('KnowledgeStore', () => {
97
86
  });
98
87
  });
99
88
 
89
+ it('treats unmarked players as suspected by default', () => {
90
+ const s = store();
91
+ s.set({ type: 'player', selector: '5', tags: ['hostile'] });
92
+
93
+ const v = view({ '5': 'Bob', '9': 'Zed' });
94
+
95
+ // 显式标记的两档
96
+ expect(v.isHostile({ seat: 5 })).toBe(true);
97
+ expect(v.isSuspect({ seat: 5 })).toBe(false);
98
+ // 完全没标记的人 = 被怀疑(markOf 返回 undefined)
99
+ expect(v.markOf({ seat: 9 })).toBeUndefined();
100
+ expect(v.isSuspect({ name: 'Zed' })).toBe(true);
101
+ expect(v.isHostile({ seat: 9 })).toBe(false);
102
+ expect(v.isTrusted({ seat: 9 })).toBe(false);
103
+ });
104
+
100
105
  it('aggregates player marks written by seat and by name', () => {
101
106
  const s = store();
102
- s.set({ type: 'player', selector: '5', tags: ['suspect'] });
107
+ s.set({ type: 'player', selector: '5', tags: ['hostile'] });
103
108
 
104
109
  const v = view({ '5': 'Bob' });
105
110
 
106
- expect(v.isSuspect({ name: 'Bob' })).toBe(true);
107
- expect(v.markOf({ seat: 5 })).toBe('suspect');
111
+ expect(v.isHostile({ name: 'Bob' })).toBe(true);
112
+ expect(v.markOf({ seat: 5 })).toBe('hostile');
108
113
  });
109
114
 
110
115
  it('uses the latest mark across seat and name selectors', () => {
@@ -127,12 +132,12 @@ describe('KnowledgeStore', () => {
127
132
  it('keeps role facts independent from strategy marks', () => {
128
133
  const s = store();
129
134
  s.set({ type: 'player', selector: '5', key: 'role', value: 'impostor' });
130
- s.set({ type: 'player', selector: '5', tags: ['suspect'] });
135
+ s.set({ type: 'player', selector: '5', tags: ['trusted'] });
131
136
 
132
137
  const v = view();
133
138
 
134
139
  expect(v.roleOf({ seat: 5 })).toBe('impostor');
135
- expect(v.markOf({ seat: 5 })).toBe('suspect');
140
+ expect(v.markOf({ seat: 5 })).toBe('trusted');
136
141
  expect(v.isHostile({ seat: 5 })).toBe(false);
137
142
  });
138
143
 
@@ -140,38 +145,23 @@ describe('KnowledgeStore', () => {
140
145
  const s = store();
141
146
  s.set({ type: 'player', selector: '5', tags: ['trusted'] });
142
147
  tick();
143
- s.set({ type: 'player', selector: 'Bob', tags: ['suspect'] });
148
+ s.set({ type: 'player', selector: 'Bob', tags: ['hostile'] });
144
149
 
145
150
  const v = view({ '5': 'Bob' });
146
151
 
147
- expect(v.isSuspect({ seat: 5 })).toBe(true);
152
+ expect(v.isHostile({ seat: 5 })).toBe(true);
148
153
  expect(v.isTrusted({ seat: 5 })).toBe(false);
149
154
  expect(v.selectorsWithTag('player', 'trusted')).toEqual([]);
150
- expect(v.selectorsWithTag('player', 'suspect')).toEqual(['Bob']);
151
- });
152
-
153
- it('applies confidence thresholds to facts and tags', () => {
154
- const s = store();
155
- s.set({ type: 'player', selector: '5', tags: ['suspect'], confidence: 0.5 });
156
- s.set({ type: 'player', selector: '6', tags: ['hostile'], confidence: 0.7 });
157
-
158
- const v = view();
159
-
160
- expect(v.isSuspect({ seat: 5 }, 0.4)).toBe(true);
161
- expect(v.isSuspect({ seat: 5 }, 0.6)).toBe(false);
162
- expect(v.isHostile({ seat: 6 }, 0.6)).toBe(true);
163
- expect(v.isHostile({ seat: 6 }, 0.8)).toBe(false);
155
+ expect(v.selectorsWithTag('player', 'hostile')).toEqual(['Bob']);
164
156
  });
165
157
 
166
- it('maps legacy tags onto the three canonical marks', () => {
158
+ it('maps legacy tags onto the two canonical marks', () => {
167
159
  const s = store();
168
- s.set({ type: 'player', selector: '5', tags: ['avoid'] });
169
160
  s.set({ type: 'player', selector: '6', tags: ['kill_if_armed'] });
170
161
  s.set({ type: 'player', selector: '7', tags: ['protected'] });
171
162
 
172
163
  const v = view();
173
164
 
174
- expect(v.markOf({ seat: 5 })).toBe('suspect');
175
165
  expect(v.markOf({ seat: 6 })).toBe('hostile');
176
166
  expect(v.markOf({ seat: 7 })).toBe('trusted');
177
167
  expect(v.hasTag({ seat: 6 }, 'hostile')).toBe(true);
@@ -5,8 +5,9 @@
5
5
  * - 跨进程:Agent(短进程 CLI)写,strategy(长进程 loop)按 mtime 轮询读,免重启。
6
6
  * - 快照而非日志:单个 JSON 对象,del/clear 直接重写;写入用临时文件 + rename 原子替换。
7
7
  * - gameId 作用域:每份知识标记所属对局(= 最新 session 文件名),换局自动失效。
8
- * - 价值中立:只存推断 + confidence/source,对 value 不做价值判断;
8
+ * - 价值中立:只存推断 + source,对 value 不做价值判断;
9
9
  * 确定事实(role/队友/尸体…)走 state/ctx.teammates,不入库。
10
+ * - 玩家两档标记:只显式标 hostile(坏人)/ trusted(好人),未标记的一律默认「被怀疑」,不再有 suspect 标记,也不存置信度。
10
11
  */
11
12
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
12
13
  import { basename, dirname, join } from 'path';
@@ -20,14 +21,12 @@ export type KnowledgeScope = 'game' | 'session' | 'persistent';
20
21
 
21
22
  export interface FactMeta {
22
23
  value: unknown;
23
- confidence?: number;
24
24
  note?: string;
25
25
  source?: KnowledgeSource;
26
26
  ts: number;
27
27
  }
28
28
 
29
29
  export interface TagMeta {
30
- confidence?: number;
31
30
  note?: string;
32
31
  source?: KnowledgeSource;
33
32
  ts: number;
@@ -38,7 +37,7 @@ export interface SubjectEntry {
38
37
  type: string; // 'player' | 'room' | 'task' | 'global' | <自定义>
39
38
  selector: string; // player→座位号或名字;room→房名;global→''
40
39
  facts?: Record<string, FactMeta>; // 键值事实,如 role: { value: 'impostor' }
41
- tags?: Record<string, TagMeta>; // 玩家策略主标记:suspect / hostile / trusted;也支持自定义标签
40
+ tags?: Record<string, TagMeta>; // 玩家策略主标记:hostile / trusted(未标记默认被怀疑);也支持自定义标签
42
41
  scope?: KnowledgeScope;
43
42
  }
44
43
 
@@ -56,7 +55,8 @@ export type KnowledgeFileReadResult =
56
55
  export type PlayerRef = { name?: string; seat?: number };
57
56
  export type SubjectRef = { type: string; selector: string };
58
57
  export type KnowledgeRef = PlayerRef | SubjectRef;
59
- export type PlayerMark = 'suspect' | 'hostile' | 'trusted';
58
+ /** 玩家显式标记只有两档:hostile(坏人)/ trusted(好人)。未标记 = 被怀疑(markOf 返回 undefined)。 */
59
+ export type PlayerMark = 'hostile' | 'trusted';
60
60
 
61
61
  export interface KnowledgeView {
62
62
  /** 当前没有任何知识时为 true。 */
@@ -65,28 +65,24 @@ export interface KnowledgeView {
65
65
  subject(type: string, selector: string): SubjectEntry | undefined;
66
66
  /** 取某 subject 的键值事实。player ref 会按座位↔名字归一化解析。 */
67
67
  getFact(ref: KnowledgeRef, key: string): FactMeta | undefined;
68
- /** 该 subject 是否带某标签(可选最低置信度门槛)。玩家三档 mark 及旧别名按最新结论解析。 */
69
- hasTag(ref: KnowledgeRef, tag: string, minConfidence?: number): boolean;
70
- /** 按 type / tag / factKey / 置信度筛选 subject。玩家 mark 标签同样遵守最新结论。 */
71
- query(q: { type?: string; tag?: string; factKey?: string; minConfidence?: number }): SubjectEntry[];
68
+ /** 该 subject 是否带某标签。玩家两档 mark 及旧别名按最新结论解析。 */
69
+ hasTag(ref: KnowledgeRef, tag: string): boolean;
70
+ /** 按 type / tag / factKey 筛选 subject。玩家 mark 标签同样遵守最新结论。 */
71
+ query(q: { type?: string; tag?: string; factKey?: string }): SubjectEntry[];
72
72
  /** 某 type 下带某标签的全部 selector(喂给 matchesAnyTarget 用)。玩家 mark 标签同样过滤旧结论。 */
73
- selectorsWithTag(type: string, tag: string, minConfidence?: number): string[];
73
+ selectorsWithTag(type: string, tag: string): string[];
74
74
  // —— player 便捷语法糖 ——
75
75
  /** 最近一次 role 事实。身份事实与策略 mark 完全解耦。 */
76
76
  roleOf(p: PlayerRef): string | undefined;
77
- markOf(p: PlayerRef, minConfidence?: number): PlayerMark | undefined;
78
- isSuspect(p: PlayerRef, minConfidence?: number): boolean;
79
- isHostile(p: PlayerRef, minConfidence?: number): boolean;
80
- isTrusted(p: PlayerRef, minConfidence?: number): boolean;
81
- /** @deprecated Use isHostile. */
82
- isConfirmedHostile(p: PlayerRef, minConfidence?: number): boolean;
83
- /** @deprecated Use isTrusted. */
84
- isCleared(p: PlayerRef): boolean;
77
+ /** 最新显式标记;未标记返回 undefined(= 被怀疑)。 */
78
+ markOf(p: PlayerRef): PlayerMark | undefined;
79
+ /** 被怀疑 = 既非 hostile 也非 trusted(含完全未标记者)。 */
80
+ isSuspect(p: PlayerRef): boolean;
81
+ isHostile(p: PlayerRef): boolean;
82
+ isTrusted(p: PlayerRef): boolean;
85
83
  }
86
84
 
87
85
  const PLAYER_MARK_ALIASES: Record<string, PlayerMark> = {
88
- suspect: 'suspect',
89
- avoid: 'suspect',
90
86
  hostile: 'hostile',
91
87
  kill_if_armed: 'hostile',
92
88
  trusted: 'trusted',
@@ -95,6 +91,14 @@ const PLAYER_MARK_ALIASES: Record<string, PlayerMark> = {
95
91
  do_not_kill: 'trusted',
96
92
  };
97
93
 
94
+ /**
95
+ * 两档化后被废弃的旧玩家标记:一律视同未标记(= 被怀疑),不映射到任何 canonical mark
96
+ *(「被怀疑」就是默认态,没有对应标签)。读取侧与写入侧共用这一份清单,避免漂移:
97
+ * - 读取侧(buildKnowledgeView.tagActive):旧数据里的 tags.suspect / tags.avoid 不再被 hasTag / query / selectorsWithTag 命中。
98
+ * - 写入侧(knowledge mark 的 removeTags):改判玩家时顺手删掉残留的 raw tag,否则 knowledge get / subject() 仍会暴露 tags.suspect/avoid 误导人。
99
+ */
100
+ export const DROPPED_PLAYER_MARK_TAGS = new Set(['suspect', 'avoid']);
101
+
98
102
  function nowTs(): number {
99
103
  return Date.now();
100
104
  }
@@ -180,7 +184,6 @@ export interface KnowledgeSetInput {
180
184
  value?: unknown;
181
185
  tags?: string[];
182
186
  removeTags?: string[];
183
- confidence?: number;
184
187
  note?: string;
185
188
  source?: KnowledgeSource;
186
189
  scope?: KnowledgeScope;
@@ -210,9 +213,6 @@ export class KnowledgeStore {
210
213
 
211
214
  set(input: KnowledgeSetInput): SubjectEntry {
212
215
  if (!input.type || !input.selector) throw new Error('knowledge set requires type and selector');
213
- if (input.confidence != null && (!Number.isFinite(input.confidence) || input.confidence < 0 || input.confidence > 1)) {
214
- throw new Error('confidence must be a finite number between 0 and 1');
215
- }
216
216
  const file = this.load();
217
217
  const key = subjectKey(input.type, input.selector);
218
218
  const entry: SubjectEntry = file.subjects[key] ?? { type: input.type, selector: input.selector };
@@ -223,7 +223,6 @@ export class KnowledgeStore {
223
223
  entry.facts = entry.facts ?? {};
224
224
  entry.facts[input.key] = {
225
225
  value: input.value === undefined ? true : input.value,
226
- confidence: input.confidence,
227
226
  note: input.note,
228
227
  source,
229
228
  ts,
@@ -233,7 +232,7 @@ export class KnowledgeStore {
233
232
  entry.tags = entry.tags ?? {};
234
233
  for (const tag of input.tags) {
235
234
  if (!tag) continue;
236
- entry.tags[tag] = { confidence: input.confidence, note: input.note, source, ts };
235
+ entry.tags[tag] = { note: input.note, source, ts };
237
236
  }
238
237
  }
239
238
  if (input.removeTags && entry.tags) {
@@ -283,7 +282,6 @@ export function buildKnowledgeView(
283
282
  ): KnowledgeView {
284
283
  const subjects = file?.subjects ?? {};
285
284
  const namesBySeat = opts.playerNamesBySeat ?? {};
286
- const confOk = (conf: number | undefined, min?: number): boolean => min == null || (conf ?? 1) >= min;
287
285
 
288
286
  const resolveAll = (ref: KnowledgeRef): SubjectEntry[] => {
289
287
  if (isSubjectRef(ref)) {
@@ -302,8 +300,9 @@ export function buildKnowledgeView(
302
300
  return best;
303
301
  };
304
302
 
305
- type MarkVerdict = { kind: PlayerMark; ts: number; confidence?: number };
306
- const markRank = (kind: PlayerMark): number => kind === 'trusted' ? 3 : kind === 'hostile' ? 2 : 1;
303
+ type MarkVerdict = { kind: PlayerMark; ts: number };
304
+ // 同一时刻打了冲突标记时,trusted 优先级高于 hostile(好人判断更慎重,不轻易被覆盖)。
305
+ const markRank = (kind: PlayerMark): number => kind === 'trusted' ? 2 : 1;
307
306
  const latestMark = (ref: KnowledgeRef): MarkVerdict | undefined => {
308
307
  let best: MarkVerdict | undefined;
309
308
  const take = (v: MarkVerdict) => {
@@ -316,56 +315,52 @@ export function buildKnowledgeView(
316
315
  for (const s of resolveAll(ref)) {
317
316
  for (const [tag, meta] of Object.entries(s.tags ?? {})) {
318
317
  const kind = PLAYER_MARK_ALIASES[tag];
319
- if (kind) take({ kind, ts: meta.ts, confidence: meta.confidence });
318
+ if (kind) take({ kind, ts: meta.ts });
320
319
  }
321
320
  }
322
321
  return best;
323
322
  };
324
323
 
325
324
  const markKindForTag = (tag: string): PlayerMark | undefined => PLAYER_MARK_ALIASES[tag];
326
- const tagActive = (s: SubjectEntry, tag: string, min?: number): boolean => {
327
- const requestedMark = markKindForTag(tag);
328
- if (s.type === 'player' && requestedMark) {
329
- const carriesRequestedMark = Object.keys(s.tags ?? {}).some(candidate => markKindForTag(candidate) === requestedMark);
330
- if (!carriesRequestedMark) return false;
331
- const ref: PlayerRef = /^\d+$/.test(s.selector.trim()) ? { seat: Number(s.selector) } : { name: s.selector };
332
- const mark = latestMark(ref);
333
- return !!mark && mark.kind === requestedMark && confOk(mark.confidence, min);
325
+ const tagActive = (s: SubjectEntry, tag: string): boolean => {
326
+ if (s.type === 'player') {
327
+ const requestedMark = markKindForTag(tag);
328
+ if (requestedMark) {
329
+ const carriesRequestedMark = Object.keys(s.tags ?? {}).some(candidate => markKindForTag(candidate) === requestedMark);
330
+ if (!carriesRequestedMark) return false;
331
+ const ref: PlayerRef = /^\d+$/.test(s.selector.trim()) ? { seat: Number(s.selector) } : { name: s.selector };
332
+ const mark = latestMark(ref);
333
+ return !!mark && mark.kind === requestedMark;
334
+ }
335
+ // 废弃的旧玩家标记(suspect/avoid)= 未标记 = 被怀疑,绝不命中;其余自定义标签(如 paradise_fish)照常按原值判定。
336
+ if (DROPPED_PLAYER_MARK_TAGS.has(tag)) return false;
334
337
  }
335
- const meta = s.tags?.[tag];
336
- return !!meta && confOk(meta.confidence, min);
338
+ return !!s.tags?.[tag];
337
339
  };
338
340
 
339
341
  return {
340
342
  empty: Object.keys(subjects).length === 0,
341
343
  subject: (type, selector) => subjects[subjectKey(type, selector)],
342
344
  getFact: (ref, key) => latestFact(ref, key),
343
- hasTag: (ref, tag, min) => resolveAll(ref).some((s) => tagActive(s, tag, min)),
345
+ hasTag: (ref, tag) => resolveAll(ref).some((s) => tagActive(s, tag)),
344
346
  query: (q) => Object.values(subjects).filter((s) => {
345
347
  if (q.type && s.type !== q.type) return false;
346
- if (q.factKey) {
347
- const f = s.facts?.[q.factKey];
348
- if (!f || !confOk(f.confidence, q.minConfidence)) return false;
349
- }
350
- if (q.tag && !tagActive(s, q.tag, q.minConfidence)) return false;
348
+ if (q.factKey && !s.facts?.[q.factKey]) return false;
349
+ if (q.tag && !tagActive(s, q.tag)) return false;
351
350
  return true;
352
351
  }),
353
- selectorsWithTag: (type, tag, min) => Object.values(subjects)
354
- .filter((s) => s.type === type && tagActive(s, tag, min))
352
+ selectorsWithTag: (type, tag) => Object.values(subjects)
353
+ .filter((s) => s.type === type && tagActive(s, tag))
355
354
  .map((s) => s.selector),
356
355
  roleOf: (p) => {
357
356
  const fact = latestFact(p, 'role');
358
357
  return typeof fact?.value === 'string' ? fact.value : undefined;
359
358
  },
360
- markOf: (p, min) => {
361
- const mark = latestMark(p);
362
- return mark && confOk(mark.confidence, min) ? mark.kind : undefined;
363
- },
364
- isSuspect: (p, min) => latestMark(p)?.kind === 'suspect' && confOk(latestMark(p)?.confidence, min),
365
- isHostile: (p, min) => latestMark(p)?.kind === 'hostile' && confOk(latestMark(p)?.confidence, min),
366
- isTrusted: (p, min) => latestMark(p)?.kind === 'trusted' && confOk(latestMark(p)?.confidence, min),
367
- isConfirmedHostile: (p, min) => latestMark(p)?.kind === 'hostile' && confOk(latestMark(p)?.confidence, min),
368
- isCleared: (p) => latestMark(p)?.kind === 'trusted',
359
+ markOf: (p) => latestMark(p)?.kind,
360
+ // 未标记 hostile/trusted 的玩家一律「被怀疑」——这就是「除好人坏人外剩下全是被怀疑」的落点。
361
+ isSuspect: (p) => !latestMark(p),
362
+ isHostile: (p) => latestMark(p)?.kind === 'hostile',
363
+ isTrusted: (p) => latestMark(p)?.kind === 'trusted',
369
364
  };
370
365
  }
371
366
 
package/src/sdk/index.ts CHANGED
@@ -21,8 +21,7 @@
21
21
  * hasKnownCorpse / corpseAtScene / nearestReportableCorpse / safePatrolStep /
22
22
  * nonTeammatesVisible / matchesTarget / isTargetAlive / nearestVisibleTarget /
23
23
  * pursueVisibleTarget / killCooldownSecs / hasKillUseRemaining / canUseKill / killRangeFor / killCommitRange /
24
- * isKnowledgeHostile / isKnowledgeThreat / isKnowledgeTrusted
25
- * (含 @deprecated 别名 isKnowledgeKillTarget / isKnowledgeAvoid / isKnowledgeProtected) /
24
+ * isKnowledgeHostile(坏人)/ isKnowledgeThreat(非 trusted = 坏人+被怀疑)/ isKnowledgeTrusted(好人)/
26
25
  * PatrolState / SafeTaskOptions / SafePatrolOptions + 常量(TASK_SUBMIT_RADIUS /
27
26
  * SHRIMP_VISION_RANGE / SHRIMP_VISION_EXIT_BUFFER / SHRIMP_VISION_RELEASE_RANGE /
28
27
  * FOLLOW_RANGE / PATROL_REACHED_DISTANCE / PROGRESS_INTERVAL_MS)
@@ -53,12 +52,12 @@ export type {
53
52
  // Game utilities (for user strategies)
54
53
  export {
55
54
  dist, firstAvailableTask, nearestKnownCorpse, nearestKnownCorpseNav, hasKnownCorpse, corpseAtScene, nearestReportableCorpse,
55
+ emergencyRushDecision, EMERGENCY_RUSH_ALIVE_THRESHOLD,
56
56
  safePatrolStep,
57
57
  nonTeammatesVisible, matchesTarget, isTargetAlive,
58
58
  nearestVisibleTarget, pursueVisibleTarget,
59
59
  killCooldownSecs, hasKillUseRemaining, canUseKill, killRangeFor, killCommitRange,
60
60
  isKnowledgeHostile, isKnowledgeThreat, isKnowledgeTrusted,
61
- isKnowledgeKillTarget, isKnowledgeAvoid, isKnowledgeProtected,
62
61
  PatrolState,
63
62
  TASK_SUBMIT_RADIUS, SHRIMP_VISION_RANGE, SHRIMP_VISION_EXIT_BUFFER, SHRIMP_VISION_RELEASE_RANGE,
64
63
  FOLLOW_RANGE, PATROL_REACHED_DISTANCE,
package/src/sdk/types.ts CHANGED
@@ -130,6 +130,8 @@ export interface GameState {
130
130
  players: PlayerInfo[];
131
131
  corpses: CorpseInfo[];
132
132
  emergency?: EmergencyInfo;
133
+ /** 服务端下发的「已知存活人数」:存活者 + 本玩家尚未目睹其死亡者之和。残局抢修紧急任务的门槛。 */
134
+ alive_count?: number;
133
135
  task_progress?: { completed: number; goal: number };
134
136
  new_events?: Array<Record<string, any>>;
135
137
  stale: boolean;
@@ -1,20 +1,19 @@
1
1
  # avoid-players 知识契约
2
2
 
3
- `avoid-players` 读取统一的玩家三档标记:
3
+ `avoid-players` 读取两档玩家标记,**未标记的一律默认「被怀疑」**:
4
4
 
5
5
  | 标记 | 行为 |
6
6
  |------|------|
7
- | `suspect` | 加入回避名单,进入视野便逃离 |
8
- | `hostile` | 加入回避名单,进入视野便逃离 |
9
- | `trusted` | 不因知识标记而回避 |
7
+ | `hostile`(坏人) | 加入回避名单,进入视野便逃离 |
8
+ | `trusted`(好人) | 不因知识标记而回避 |
9
+ | 未标记(被怀疑) | 不自动回避——照常按房间巡逻(需要躲谁就把它标 hostile,或用启动参数点名) |
10
10
 
11
- 回避名单 = 启动参数(座位号/名字)∪ suspect/hostile 标记。威胁消失后恢复巡逻。
11
+ 回避名单 = 启动参数(座位号/名字)∪ 知识库 hostile 标记。威胁消失后恢复巡逻。
12
12
 
13
13
  ```bash
14
- ccl knowledge mark 4 suspect --confidence 0.7 --note "贴脸尾随"
15
- ccl knowledge mark 4 hostile --confidence 0.9 --note "确认危险"
14
+ ccl knowledge mark 4 hostile --note "确认危险"
16
15
  ccl knowledge mark 4 trusted --note "已澄清"
17
- ccl knowledge del player 4
16
+ ccl knowledge del player 4 # 取消标记 → 回到默认「被怀疑」(不自动回避)
18
17
  ```
19
18
 
20
19
  `role` 是纯身份事实,不直接控制策略。
@@ -5,7 +5,7 @@ import { parseTargetArgs } from './player-targets.js';
5
5
 
6
6
  export const strategy: StrategyEntry = {
7
7
  id: 'avoid-players',
8
- description: '回避玩家:按房间顺序巡逻,视野里出现回避目标就寻路躲避(推演逃点、持续远离),威胁消失后恢复巡逻。回避名单 = 启动参数(座位号或名字,可多人,可省略)∪ 知识库中标记为 suspect/hostile 的玩家;trusted 不回避。Agent 可用 `ccl knowledge mark` 动态改判,免重启。',
8
+ description: '回避玩家:按房间顺序巡逻,视野里出现回避目标就寻路躲避(推演逃点、持续远离),威胁消失后恢复巡逻。回避名单 = 启动参数(座位号或名字,可多人,可省略)∪ 知识库中标记为 hostile(坏人)的玩家;trusted 与未标记的被怀疑者都不自动回避(要躲就标 hostile 或用启动参数点名)。Agent 可用 `ccl knowledge mark` 动态改判,免重启。',
9
9
  create(args?: string[]) {
10
10
  const targets = args ? parseTargetArgs(args) : [];
11
11
  return new GoalRootStrategy('avoid-players', () => new AvoidPlayersTop(targets), {
@@ -15,7 +15,7 @@ class CorpsePatrolStrategy extends GoalRootStrategy {
15
15
 
16
16
  export const strategy: StrategyEntry = {
17
17
  id: 'corpse-patrol',
18
- description: '发现尸体就在附近40至100距离内随机游荡,但不报告——故意在尸体旁出没制造嫌疑感。没有尸体时巡逻各房间。传入打招呼话术时,视野内出现人就随机发送一条,之后120秒内不再发言。不做任务,不杀人。(天堂鱼默认;进阶版见 paradise-fish)参数:可选:1~3 条打招呼话术。',
18
+ description: '发现尸体就在附近40至100距离内随机游荡,但不报告——故意在尸体旁出没制造嫌疑感。没有尸体时巡逻各房间。传入打招呼话术时,视野内出现人就随机发送一条,之后120秒内不再发言。不做任务,不杀人;但残局(已知存活≤6)出现紧急维修任务时例外,会最高优先级抢着去做,阻止蟹靠破坏倒计时取胜。(天堂鱼默认;进阶版见 paradise-fish)参数:可选:1~3 条打招呼话术。',
19
19
  create(args?: string[]) {
20
20
  return new CorpsePatrolStrategy(parseGreetingArgs(args, 'corpse-patrol'));
21
21
  },
@@ -327,6 +327,26 @@ export function taskMoveDecision(state: GameState, ctx: StrategyContext, task: T
327
327
  return { action: Action.move({ x: task.x!, y: task.y! }) };
328
328
  }
329
329
 
330
+ /** 已知存活人数 ≤ 此值才触发「残局抢修紧急任务」。 */
331
+ export const EMERGENCY_RUSH_ALIVE_THRESHOLD = 6;
332
+
333
+ /**
334
+ * 残局抢修紧急维修任务的「靠近 → 到点提交」决策:已知存活人数(state.alive_count,含本玩家尚未目睹其
335
+ * 死亡者)≤ EMERGENCY_RUSH_ALIVE_THRESHOLD 且当前有进行中的紧急任务时返回该单步决策,否则返回 null
336
+ * (行为不触发,调用方按自身原优先级继续)。
337
+ *
338
+ * 背景:紧急任务超时则蟹阵营直接获胜(后端倒计时),而中立的章鱼/天堂鱼要靠活到最后取胜,因此残局应抢着
339
+ * 把维修做掉、掐断蟹的破坏取胜线。服务端已放开中立/蟹阵营提交紧急任务。紧急任务被任意玩家完成 / 开会 /
340
+ * 超时后,服务端会清空 state.emergency(→ ctx.emergency 为 null),本函数随之自动返回 null。
341
+ */
342
+ export function emergencyRushDecision(state: GameState, ctx: StrategyContext): BehaviorDecision | null {
343
+ if (typeof state.alive_count !== 'number' || state.alive_count > EMERGENCY_RUSH_ALIVE_THRESHOLD) {
344
+ return null;
345
+ }
346
+ const emergency = firstAvailableTask([], () => true, ctx.emergency, ctx.blockedMoveTarget);
347
+ return emergency ? taskMoveDecision(state, ctx, emergency) : null;
348
+ }
349
+
330
350
  export function nearestReportableCorpse(state: GameState): GameState['corpses'][number] | null {
331
351
  const nearby = state.corpses
332
352
  .map(corpse => {
@@ -666,29 +686,21 @@ function knowledgeOf(ctx: StrategyContext) {
666
686
  return ctx.knowledge ?? FALLBACK_KNOWLEDGE;
667
687
  }
668
688
 
669
- /** hostile:带刀策略可主动攻击;无刀策略应回避。 */
670
- export function isKnowledgeHostile(p: PlayerRef, ctx: StrategyContext, minConfidence?: number): boolean {
671
- return knowledgeOf(ctx).isHostile(p, minConfidence);
689
+ /** hostile(坏人):明确敌对。带刀策略刀好时可主动攻击;无刀策略硬回避。 */
690
+ export function isKnowledgeHostile(p: PlayerRef, ctx: StrategyContext): boolean {
691
+ return knowledgeOf(ctx).isHostile(p);
672
692
  }
673
693
 
674
- /** suspect hostile:任何生存型策略都应视为威胁并回避。 */
675
- export function isKnowledgeThreat(p: PlayerRef, ctx: StrategyContext, minConfidence?: number): boolean {
676
- const mark = knowledgeOf(ctx).markOf(p, minConfidence);
677
- return mark === 'suspect' || mark === 'hostile';
694
+ /** 威胁 = trusted(坏人 + 被怀疑)。未标记 trusted 的人一律按需警惕——「除好人外都值得提防」。 */
695
+ export function isKnowledgeThreat(p: PlayerRef, ctx: StrategyContext): boolean {
696
+ return !knowledgeOf(ctx).isTrusted(p);
678
697
  }
679
698
 
680
- /** trusted:可信同伴,永远优先于回避与击杀。 */
681
- export function isKnowledgeTrusted(p: PlayerRef, ctx: StrategyContext, minConfidence?: number): boolean {
682
- return knowledgeOf(ctx).isTrusted(p, minConfidence);
699
+ /** trusted(好人):可信同伴,永远优先于回避与击杀。 */
700
+ export function isKnowledgeTrusted(p: PlayerRef, ctx: StrategyContext): boolean {
701
+ return knowledgeOf(ctx).isTrusted(p);
683
702
  }
684
703
 
685
- /** @deprecated Use isKnowledgeHostile. */
686
- export const isKnowledgeKillTarget = isKnowledgeHostile;
687
- /** @deprecated Use isKnowledgeThreat. */
688
- export const isKnowledgeAvoid = isKnowledgeThreat;
689
- /** @deprecated Use isKnowledgeTrusted. */
690
- export const isKnowledgeProtected = isKnowledgeTrusted;
691
-
692
704
  export function resolveRoom(name: string, ctx: StrategyContext): RoomTarget | null {
693
705
  const folded = name.trim().toLowerCase();
694
706
  if (!folded) return null;
@@ -1,5 +1,5 @@
1
1
  import type { GameState, PlayerInfo } from '../../sdk/types.js';
2
- import { hasReachedRoomTarget, isKnowledgeThreat, isTargetAlive, matchesAnyTarget, PatrolState, PROGRESS_INTERVAL_MS } from '../game-utils.js';
2
+ import { hasReachedRoomTarget, isKnowledgeHostile, isTargetAlive, matchesAnyTarget, PatrolState, PROGRESS_INTERVAL_MS } from '../game-utils.js';
3
3
  import type { BehaviorDecision, RoomTarget, StrategyContext } from '../types.js';
4
4
  import { Goal } from './goal.js';
5
5
  import { KeepAwayGoal, type KeepAwayGoalOptions, type ThreatResolver } from './keep-away-goal.js';
@@ -62,10 +62,10 @@ export class AvoidPlayersTop extends Goal {
62
62
  this.moveMode = 'flee';
63
63
  }
64
64
 
65
- /** args 回避名单(活着的)∪ 知识里标记 suspect/hostile 的玩家。 */
65
+ /** args 回避名单(活着的)∪ 知识库标记 hostile(坏人)的玩家。被怀疑者(未标记)不自动回避——照常按房间巡逻。 */
66
66
  private threats(state: GameState, ctx: StrategyContext): PlayerInfo[] {
67
67
  const validTargets = this.targets.filter(target => isTargetAlive(state, target, ctx));
68
- return state.players.filter(p => matchesAnyTarget(p, validTargets, ctx) || isKnowledgeThreat(p, ctx));
68
+ return state.players.filter(p => matchesAnyTarget(p, validTargets, ctx) || isKnowledgeHostile(p, ctx));
69
69
  }
70
70
 
71
71
  private setMoveGoal(room: RoomTarget, priority: number, mode: 'patrol' | 'flee', stopOnPlayer: boolean): void {
@@ -2,6 +2,7 @@ import type { GameState } from '../../sdk/types.js';
2
2
  import { Action } from '../../sdk/action.js';
3
3
  import {
4
4
  corpseAtScene,
5
+ emergencyRushDecision,
5
6
  nearestKnownCorpseNav,
6
7
  patrolStep,
7
8
  PatrolState,
@@ -24,6 +25,14 @@ export class CorpsePatrolTop extends Goal {
24
25
  }
25
26
 
26
27
  tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
28
+ // 残局抢修紧急任务(已知存活≤6):天堂鱼最高优先级,先于贴尸/巡逻/打招呼——
29
+ // 紧急任务超时则蟹直接获胜,天堂鱼靠活到最后取胜,故残局抢着把维修做掉、掐断蟹的破坏取胜线。
30
+ const emergency = emergencyRushDecision(state, ctx);
31
+ if (emergency) {
32
+ this.emitProgress(state, ctx, null, true);
33
+ return [emergency];
34
+ }
35
+
27
36
  const decisions: BehaviorDecision[] = [];
28
37
 
29
38
  const greetingDecision = this.tryGreeting(state);
@@ -63,13 +72,26 @@ export class CorpsePatrolTop extends Goal {
63
72
  return { action: Action.speech(text) };
64
73
  }
65
74
 
66
- private emitProgress(state: GameState, ctx: StrategyContext, corpse: CorpseTarget | null): void {
75
+ private emitProgress(
76
+ state: GameState,
77
+ ctx: StrategyContext,
78
+ corpse: CorpseTarget | null,
79
+ emergencyRush = false,
80
+ ): void {
67
81
  const now = Date.now();
68
82
  if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
69
83
  ctx.lastProgressNotifyAt = now;
70
84
 
71
85
  const room = state.you.room ?? '未知';
72
86
 
87
+ if (emergencyRush) {
88
+ const name = ctx.emergency?.task_name ?? '紧急维修';
89
+ ctx.notifications.push(
90
+ `[进度] 当前在${room},残局仅剩${state.alive_count}人,抢着去做紧急任务「${name}」,阻止蟹靠破坏倒计时取胜。`,
91
+ );
92
+ return;
93
+ }
94
+
73
95
  let msg = `[进度] 当前在${room}`;
74
96
  if (corpse) {
75
97
  const label = corpse.name ?? '未知';
@@ -2,6 +2,8 @@ import type { BehaviorDecision } from '../types.js';
2
2
  import { Goal } from './goal.js';
3
3
 
4
4
  export const URGENT_GOAL_PRIORITY = 1;
5
+ /** 残局抢修紧急任务:高于游走任务、低于落单猎杀/反射(URGENT),故对发言仍可被打断(< URGENT)。 */
6
+ export const EMERGENCY_GOAL_PRIORITY = 0.6;
5
7
  export const WANDER_GOAL_PRIORITY = 0.2;
6
8
 
7
9
  export class LeafGoal extends Goal {