@ganglion/xacpx 0.9.2 → 0.10.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.
@@ -3,10 +3,10 @@ import type { ScheduledSessionMode } from "../../scheduled/scheduled-types";
3
3
  import type { HelpTopicMetadata } from "../help/help-types";
4
4
  export declare function laterHelp(): HelpTopicMetadata;
5
5
  export declare function handleLaterHelp(): RouterResponse;
6
- export declare function handleLaterCreate(tokens: string[], scheduled: ScheduledRouterOps, chatKey: string, currentSession: {
6
+ export declare function handleLaterCreate(tokens: string[], tails: string[], scheduled: ScheduledRouterOps, chatKey: string, currentSession: {
7
7
  alias: string;
8
8
  agent: string;
9
9
  workspace: string;
10
10
  } | null, defaultMode: ScheduledSessionMode, accountId?: string, replyContextToken?: string): Promise<RouterResponse>;
11
- export declare function handleLaterList(scheduled: ScheduledRouterOps): RouterResponse;
12
- export declare function handleLaterCancel(id: string, scheduled: ScheduledRouterOps): Promise<RouterResponse>;
11
+ export declare function handleLaterList(scheduled: ScheduledRouterOps, chatKey: string): RouterResponse;
12
+ export declare function handleLaterCancel(id: string, scheduled: ScheduledRouterOps, chatKey: string): Promise<RouterResponse>;
@@ -162,6 +162,7 @@ export type ParsedCommand = {
162
162
  } | {
163
163
  kind: "later.create";
164
164
  tokens: string[];
165
+ tails: string[];
165
166
  } | {
166
167
  kind: "later.list";
167
168
  } | {
@@ -41,7 +41,7 @@ export interface ActiveHumanQuestionPackageContext {
41
41
  }>;
42
42
  queuedCount: number;
43
43
  }
44
- export type WritableConfigStore = Pick<ConfigStore, "load" | "save" | "upsertWorkspace" | "removeWorkspace" | "upsertAgent" | "removeAgent" | "updateTransport" | "updateChannel">;
44
+ export type WritableConfigStore = Pick<ConfigStore, "load" | "upsertWorkspace" | "removeWorkspace" | "upsertAgent" | "removeAgent" | "updateTransport" | "updateChannel" | "getRawValue" | "setRawValue" | "unsetRawValue">;
45
45
  export interface CommandRouterContext {
46
46
  sessions: SessionService;
47
47
  transport: SessionTransport;
@@ -1,13 +1,52 @@
1
- import type { AppConfig } from "./types";
1
+ import type { AgentConfig, AppConfig, ChannelRuntimeConfig, PluginConfig } from "./types";
2
+ /**
3
+ * Raw-patch config persistence.
4
+ *
5
+ * The parsed `AppConfig` is a READ model only: parsing drops unknown keys
6
+ * (e.g. `workspaces.*.allowed_agents`), expands `~` in workspace cwds, and
7
+ * materializes every default. Serializing it back would destroy a hand-edited
8
+ * config.json. Every mutation therefore patches the raw JSON document read
9
+ * straight from disk, touching only the targeted subtree, and never writes a
10
+ * parsed config object.
11
+ */
12
+ export type RawConfigPathSegment = string | {
13
+ /** Addresses the entry of a JSON array whose `id` property equals this value. */
14
+ id: string;
15
+ /** When set, a missing entry is materialized from this template on writes. */
16
+ createWith?: Record<string, unknown>;
17
+ };
18
+ export type RawConfigPath = readonly RawConfigPathSegment[];
19
+ export type RawConfigLookup = {
20
+ present: true;
21
+ value: unknown;
22
+ } | {
23
+ present: false;
24
+ };
2
25
  export declare class ConfigStore {
3
26
  private readonly path;
4
27
  constructor(path: string);
5
28
  load(): Promise<AppConfig>;
6
- save(config: AppConfig): Promise<void>;
29
+ /** Reads the raw (unparsed) value at `path`, e.g. to capture it for a rollback. */
30
+ getRawValue(path: RawConfigPath): Promise<RawConfigLookup>;
31
+ setRawValue(path: RawConfigPath, value: unknown): Promise<AppConfig>;
32
+ unsetRawValue(path: RawConfigPath): Promise<AppConfig>;
7
33
  upsertWorkspace(name: string, cwd: string, description?: string): Promise<AppConfig>;
8
34
  removeWorkspace(name: string): Promise<AppConfig>;
9
- upsertAgent(name: string, agent: AppConfig["agents"][string]): Promise<AppConfig>;
35
+ upsertAgent(name: string, agent: AgentConfig): Promise<AppConfig>;
10
36
  removeAgent(name: string): Promise<AppConfig>;
11
- updateTransport(transport: Partial<AppConfig["transport"]>): Promise<AppConfig>;
12
- updateChannel(channel: Partial<AppConfig["channel"]>): Promise<AppConfig>;
37
+ /** Sets only the given transport keys; a key explicitly set to `undefined` is removed. */
38
+ updateTransport(patch: Partial<AppConfig["transport"]>): Promise<AppConfig>;
39
+ /** Sets only the given channel keys; a key explicitly set to `undefined` is removed. */
40
+ updateChannel(patch: Partial<AppConfig["channel"]>): Promise<AppConfig>;
41
+ /** Replaces the tool-managed `plugins` array; everything else stays untouched. */
42
+ replacePlugins(plugins: PluginConfig[]): Promise<AppConfig>;
43
+ /** Replaces the tool-managed `channels` array; everything else stays untouched. */
44
+ replaceChannels(channels: ChannelRuntimeConfig[]): Promise<AppConfig>;
45
+ private patchRaw;
46
+ private readRaw;
13
47
  }
48
+ export declare function serializeRawConfig(raw: Record<string, unknown>): string;
49
+ export declare function readRawConfigValue(root: Record<string, unknown>, path: RawConfigPath): RawConfigLookup;
50
+ export declare function setRawConfigValue(root: Record<string, unknown>, path: RawConfigPath, value: unknown): void;
51
+ export declare function unsetRawConfigValue(root: Record<string, unknown>, path: RawConfigPath): void;
52
+ export declare function assertSafeConfigKey(key: string): void;
@@ -7,6 +7,12 @@ export type WechatReplyMode = ReplyMode;
7
7
  export interface ChannelConfig {
8
8
  type: string;
9
9
  replyMode: ReplyMode;
10
+ /**
11
+ * Sender ids the operator trusts as channel owners. Group turns from these
12
+ * senders pass owner-gated command authorization even when the channel
13
+ * protocol carries no group-role information (e.g. WeChat).
14
+ */
15
+ ownerIds?: string[];
10
16
  options?: Record<string, unknown>;
11
17
  }
12
18
  /** @deprecated Legacy input shape only. Use ChannelConfig. */
@@ -67,6 +73,8 @@ export interface ChannelRuntimeConfig {
67
73
  type: string;
68
74
  enabled: boolean;
69
75
  replyMode?: ReplyMode;
76
+ /** See ChannelConfig.ownerIds — per-channel trusted owner sender ids. */
77
+ ownerIds?: string[];
70
78
  options?: Record<string, unknown>;
71
79
  }
72
80
  export interface PluginConfig {
@@ -11,6 +11,7 @@ export interface SessionMessages {
11
11
  currentLabel: string;
12
12
  sessionListItem: (alias: string, agent: string, workspace: string) => string;
13
13
  sessionCreated: (alias: string) => string;
14
+ sessionAlreadyExists: (alias: string, agent: string, workspace: string) => string;
14
15
  sessionAttachNotFound: (alias: string, agent: string, workspace: string) => string;
15
16
  sessionAttached: (alias: string) => string;
16
17
  switched: (alias: string, agent: string, workspace: string) => string;
@@ -43,6 +44,7 @@ export interface SessionMessages {
43
44
  sessionBlockedByTasksHint: string;
44
45
  sessionRemoved: (alias: string) => string;
45
46
  sessionRemovedWasActive: string;
47
+ sessionRemovedWasActivePromoted: (alias: string) => string;
46
48
  sessionTransportShared: (transportSession: string, count: number) => string;
47
49
  sessionOrchestrationPurgeFailed: (warning: string) => string;
48
50
  sessionTransportTeardownFailed: (warning: string) => string;
@@ -635,7 +637,6 @@ export interface CliUpdateMessages {
635
637
  updateFailed: (name: string, error: string) => string;
636
638
  targetNotFound: (name: string) => string;
637
639
  targetVersionUnknown: (name: string) => string;
638
- targetNotPinned: (name: string) => string;
639
640
  multiTargetNonInteractive: string;
640
641
  selectionPrompt: string;
641
642
  selectionInvalid: (part: string) => string;
@@ -690,6 +691,8 @@ export interface PluginCliMessages {
690
691
  noPlugins: string;
691
692
  pluginListHeader: string;
692
693
  unrecognizedArgs: (args: string) => string;
694
+ pluginSpecHasDoubleQuote: (spec: string) => string;
695
+ pluginSpecHasPercentOnWindows: (spec: string) => string;
693
696
  pluginInstallFailed: (packageSpec: string, error: string) => string;
694
697
  pluginValidateFailed: (recordedName: string, error: string) => string;
695
698
  pluginInstalled: (recordedName: string) => string;
@@ -776,8 +779,6 @@ export interface WeixinMessages {
776
779
  debugEnabled: string;
777
780
  debugDisabled: string;
778
781
  sessionCleared: string;
779
- noAccountsLoggedIn: string;
780
- logoutSuccess: string;
781
782
  commandFailed: (detail: string) => string;
782
783
  }
783
784
  export interface MigrateMessages {
@@ -803,6 +804,7 @@ export interface MiscMessages {
803
804
  quotaHeadsUp: string;
804
805
  quotaOverflowSummary: (count: number) => string;
805
806
  finalHeadsUp: (total: number, sentSoFar: number, remaining: number) => string;
807
+ finalAllParked: (count: number) => string;
806
808
  quotedMessagePrefix: (parts: string) => string;
807
809
  scheduledTaskFailed: (message: string) => string;
808
810
  orchestrationTaskCompleted: (taskId: string, workerSession: string, result: string) => string;
@@ -825,6 +827,8 @@ export interface MiscMessages {
825
827
  delegateQPackageInstr3: string;
826
828
  commandAccessDeniedSuffix: string;
827
829
  commandAccessDeniedHint: string;
830
+ commandAccessDeniedChatTypeMissingSuffix: string;
831
+ commandAccessDeniedChatTypeMissingHint: string;
828
832
  commandLabelThisMessage: string;
829
833
  sessionResetNoCurrentSession: string;
830
834
  sessionResetFailed: (alias: string) => string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The orchestration coordinator identity is derived from a session's transport
3
+ * name. `/clear` rotates that name from `workspace:alias` to
4
+ * `workspace:alias:reset-<timestamp>` (see session-reset-handler), which would
5
+ * otherwise orphan every task delegated before the reset. Stripping the
6
+ * volatile `:reset-<digits>` suffix yields the stable `workspace:alias` identity
7
+ * so ownership survives `/clear`.
8
+ *
9
+ * Pure leaf module: do not add imports, so it can be used from sessions/,
10
+ * commands/, and orchestration/ without risking an import cycle.
11
+ *
12
+ * No-op on any value lacking a trailing `:reset-<digits>` segment, so external
13
+ * coordinators (`external_*`) and normal sessions pass through unchanged.
14
+ */
15
+ export declare function stableCoordinatorSession(transportSession: string): string;
16
+ /**
17
+ * The single chokepoint for asking "do these two transport names refer to the
18
+ * same coordinator?". Both sides are reduced to their stable identity before
19
+ * comparison, so it is robust to either side carrying a volatile
20
+ * `:reset-<digits>` suffix (a live post-`/clear` session, or a legacy
21
+ * state.json record persisted before the identity was normalized at write).
22
+ *
23
+ * Every coordinator-ownership comparison must go through this rather than a raw
24
+ * `===`, so the normalization rule lives in one place instead of being
25
+ * re-derived (and inconsistently forgotten) at each call site.
26
+ */
27
+ export declare function sameCoordinatorSession(a: string, b: string): boolean;
@@ -206,23 +206,6 @@ export interface CleanTasksResult {
206
206
  removedTasks: number;
207
207
  removedBindings: number;
208
208
  }
209
- export type ResetGcTrigger = "startup" | "interval";
210
- export interface PurgeExpiredResetCoordinatorsInput {
211
- cutoffDays: number;
212
- trigger: ResetGcTrigger;
213
- }
214
- export interface PurgeExpiredResetCoordinatorsResult {
215
- candidates: number;
216
- purgedCoordinators: number;
217
- removed: {
218
- tasks: number;
219
- workerBindings: number;
220
- groups: number;
221
- coordinatorRoutes: number;
222
- humanQuestionPackages: number;
223
- coordinatorQuestionState: number;
224
- };
225
- }
226
209
  export interface OrchestrationTaskFilter {
227
210
  sourceHandle?: string;
228
211
  coordinatorSession?: string;
@@ -361,7 +344,6 @@ export declare class OrchestrationService {
361
344
  cleanTasks(coordinatorSession: string): Promise<CleanTasksResult>;
362
345
  listSessionBlockingTasks(transportSession: string): Promise<OrchestrationTaskRecord[]>;
363
346
  purgeSessionReferences(transportSession: string): Promise<CleanTasksResult>;
364
- purgeExpiredResetCoordinators(input: PurgeExpiredResetCoordinatorsInput): Promise<PurgeExpiredResetCoordinatorsResult>;
365
347
  listPendingCoordinatorResults(coordinatorSession: string): Promise<OrchestrationTaskRecord[]>;
366
348
  listPendingCoordinatorBlockers(coordinatorSession: string): Promise<OrchestrationTaskRecord[]>;
367
349
  listContestedCoordinatorResults(coordinatorSession: string): Promise<OrchestrationTaskRecord[]>;
@@ -439,11 +421,6 @@ export declare class OrchestrationService {
439
421
  private ensureGroups;
440
422
  private removeEmptyGroupsForCoordinator;
441
423
  private removeCoordinatorMetadataIfUnused;
442
- private isResetCoordinatorSession;
443
- private collectResetCoordinatorCandidates;
444
- private parseDateMs;
445
- private resolveResetCoordinatorActivityAtMs;
446
- private cascadeRemoveCoordinatorRecords;
447
424
  private bumpGroupUpdated;
448
425
  private getLatestDeliveredPackageMessage;
449
426
  private snapshotCoordinatorDeliveryRoute;
@@ -13,6 +13,7 @@ export interface CreatePerfLogWriterOptions {
13
13
  appendImpl?: (path: string, data: string) => Promise<void>;
14
14
  mkdirImpl?: (path: string, opts: {
15
15
  recursive: true;
16
+ mode?: number;
16
17
  }) => Promise<void>;
17
18
  now?: () => Date;
18
19
  }
@@ -68,6 +68,11 @@ var init_session = __esm(() => {
68
68
  currentLabel: "[current]",
69
69
  sessionListItem: (alias, agent, workspace) => `- ${alias} (${agent} @ ${workspace})`,
70
70
  sessionCreated: (alias) => `Session "${alias}" created and switched.`,
71
+ sessionAlreadyExists: (alias, agent, workspace) => [
72
+ `Session "${alias}" already exists (${agent} @ ${workspace}).`,
73
+ `Switch to it with /use ${alias}, or remove it first with /session rm ${alias}.`
74
+ ].join(`
75
+ `),
71
76
  sessionAttachNotFound: (alias, agent, workspace) => [
72
77
  "No existing session found to attach.",
73
78
  `Check the session name and retry: /session attach ${alias} --agent ${agent} --ws ${workspace} --name <session-name>`
@@ -104,6 +109,7 @@ var init_session = __esm(() => {
104
109
  sessionBlockedByTasksHint: "Use /tasks to list tasks, or /task cancel <id> to cancel one.",
105
110
  sessionRemoved: (alias) => `Session "${alias}" removed.`,
106
111
  sessionRemovedWasActive: "This was the active session. Its chat context has been cleared.",
112
+ sessionRemovedWasActivePromoted: (alias) => `This was the active session. Switched back to the previous session "${alias}".`,
107
113
  sessionTransportShared: (transportSession, count) => `Note: backend session "${transportSession}" is still referenced by ${count} other session(s) and was not closed.`,
108
114
  sessionOrchestrationPurgeFailed: (warning) => `Note: failed to purge orchestration references (${warning}). Run /tasks clean manually to clean up.`,
109
115
  sessionTransportTeardownFailed: (warning) => `Note: backend session could not be closed automatically (${warning}). Run acpx sessions close manually if needed.`,
@@ -397,7 +403,7 @@ var init_later = __esm(() => {
397
403
  helpNote2: "Time must be at least 10 seconds and at most 7 days away",
398
404
  helpNote3: "By default runs in a new temporary session that is destroyed after completion",
399
405
  helpNote4: "Use --bind to send to the session that was current when the task was created (configurable via later.defaultMode)",
400
- helpNote5: "/lt list shows all pending tasks globally; in group chats only the owner can cancel",
406
+ helpNote5: "/lt list shows only this chat's pending tasks; in group chats only the owner can cancel",
401
407
  helpNote6: "Scheduling slash-prefixed xacpx commands is not supported",
402
408
  helpNote7: "Full time format reference: docs/later-command.md"
403
409
  };
@@ -826,7 +832,6 @@ var init_cli_update = __esm(() => {
826
832
  updateFailed: (name, error) => `${name} update failed: ${error}`,
827
833
  targetNotFound: (name) => `Update target not found: ${name}`,
828
834
  targetVersionUnknown: (name) => `${name}: cannot check latest version; skipped.`,
829
- targetNotPinned: (name) => `${name} has no recorded version; use \`xacpx plugin update ${name}\` or specify a version explicitly.`,
830
835
  multiTargetNonInteractive: "Installed plugins detected; in non-interactive mode use `xacpx update --all` or `xacpx update <name>`.",
831
836
  selectionPrompt: "Select items to update (numbers, comma-separated; a=all; Enter to cancel): ",
832
837
  selectionInvalid: (part) => `Invalid selection: ${part}`,
@@ -891,6 +896,8 @@ var init_plugin_cli = __esm(() => {
891
896
  noPlugins: "No plugins installed yet.",
892
897
  pluginListHeader: "Plugins:",
893
898
  unrecognizedArgs: (args) => `Unrecognized arguments: ${args}`,
899
+ pluginSpecHasDoubleQuote: (spec) => `Invalid plugin spec ${spec}: double quotes (") are never valid in an npm package spec.`,
900
+ pluginSpecHasPercentOnWindows: (spec) => `Invalid plugin spec ${spec}: "%" would be mangled by cmd.exe on Windows. Install the package with npm directly instead.`,
894
901
  pluginInstallFailed: (packageSpec, error) => `Plugin ${packageSpec} install failed: ${error}`,
895
902
  pluginValidateFailed: (recordedName, error) => `Plugin ${recordedName} validation failed: ${error}`,
896
903
  pluginInstalled: (recordedName) => `Plugin ${recordedName} installed`,
@@ -1002,8 +1009,6 @@ var init_weixin = __esm(() => {
1002
1009
  debugEnabled: "Debug mode enabled",
1003
1010
  debugDisabled: "Debug mode disabled",
1004
1011
  sessionCleared: "✅ Session cleared. Starting a fresh conversation.",
1005
- noAccountsLoggedIn: "No accounts are currently logged in.",
1006
- logoutSuccess: "✅ Logged out. All account credentials cleared.",
1007
1012
  commandFailed: (detail) => `❌ Command failed: ${detail}`
1008
1013
  };
1009
1014
  });
@@ -1041,6 +1046,7 @@ var init_misc = __esm(() => {
1041
1046
  quotaOverflowSummary: (count) => `(${count} progress updates omitted due to message limit; see final result below)`,
1042
1047
  finalHeadsUp: (total, sentSoFar, remaining) => `—
1043
1048
  \uD83D\uDCC4 Result: ${total} parts total, ${sentSoFar} sent. Reply /jx to see the next ${remaining} parts.`,
1049
+ finalAllParked: (count) => `\uD83D\uDCC4 Message limit reached: the result (${count} parts) is parked. Reply /jx to receive it.`,
1044
1050
  quotedMessagePrefix: (parts) => `[Quote: ${parts}]`,
1045
1051
  scheduledTaskFailed: (message) => `Scheduled task failed: ${message}`,
1046
1052
  orchestrationTaskCompleted: (taskId, workerSession, result) => `Delegation task "${taskId}" completed
@@ -1069,6 +1075,8 @@ var init_misc = __esm(() => {
1069
1075
  delegateQPackageInstr3: "Do not forward the human's exact words to the worker",
1070
1076
  commandAccessDeniedSuffix: " is restricted to group owner only.",
1071
1077
  commandAccessDeniedHint: "To perform control operations, have the owner send them in the group, or use a private chat.",
1078
+ commandAccessDeniedChatTypeMissingSuffix: " was blocked: this channel did not report the chat type (direct or group), so control commands are disabled here.",
1079
+ commandAccessDeniedChatTypeMissingHint: "Read-only commands and prompts still work. This is a channel metadata issue — update or report the channel plugin.",
1072
1080
  commandLabelThisMessage: "This message",
1073
1081
  sessionResetNoCurrentSession: "No session is currently selected. Run /session new ... or /use <alias> first.",
1074
1082
  sessionResetFailed: (alias) => `Session "${alias}" reset failed. The new backend session was not created, please try again later.`,
@@ -1139,6 +1147,11 @@ var init_session2 = __esm(() => {
1139
1147
  currentLabel: "[当前]",
1140
1148
  sessionListItem: (alias, agent2, workspace2) => `- ${alias} (${agent2} @ ${workspace2})`,
1141
1149
  sessionCreated: (alias) => `会话「${alias}」已创建并切换`,
1150
+ sessionAlreadyExists: (alias, agent2, workspace2) => [
1151
+ `会话「${alias}」已存在(${agent2} @ ${workspace2})。`,
1152
+ `发送 /use ${alias} 切换到它,或先执行 /session rm ${alias} 删除后再创建。`
1153
+ ].join(`
1154
+ `),
1142
1155
  sessionAttachNotFound: (alias, agent2, workspace2) => [
1143
1156
  "没有找到可绑定的已有会话。",
1144
1157
  `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent2} --ws ${workspace2} --name <会话名>`
@@ -1175,6 +1188,7 @@ var init_session2 = __esm(() => {
1175
1188
  sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
1176
1189
  sessionRemoved: (alias) => `已删除会话「${alias}」。`,
1177
1190
  sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
1191
+ sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
1178
1192
  sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
1179
1193
  sessionOrchestrationPurgeFailed: (warning) => `提示:清理任务编排引用失败(${warning}),请稍后执行 /tasks clean 手动清理。`,
1180
1194
  sessionTransportTeardownFailed: (warning) => `提示:后端会话未能自动关闭(${warning}),如有残留请手动执行 acpx sessions close。`,
@@ -1468,7 +1482,7 @@ var init_later2 = __esm(() => {
1468
1482
  helpNote2: "时间必须在 10 秒之后、7 天之内",
1469
1483
  helpNote3: "默认在为本次任务新建的临时会话里执行,跑完即销毁",
1470
1484
  helpNote4: "加 --bind 改为发送到创建时绑定的当前会话(默认模式可用 later.defaultMode 配置)",
1471
- helpNote5: "/lt list 显示全局待执行任务;群聊中只有群主可取消",
1485
+ helpNote5: "/lt list 只显示本聊天创建的待执行任务;群聊中只有群主可取消",
1472
1486
  helpNote6: "不支持延迟执行 / 开头的 xacpx 命令",
1473
1487
  helpNote7: "完整时间格式与说明见 docs/later-command.md"
1474
1488
  };
@@ -1897,7 +1911,6 @@ var init_cli_update2 = __esm(() => {
1897
1911
  updateFailed: (name, error) => `${name} 更新失败:${error}`,
1898
1912
  targetNotFound: (name) => `没有找到更新项:${name}`,
1899
1913
  targetVersionUnknown: (name) => `${name} 无法检查最新版本,已跳过。`,
1900
- targetNotPinned: (name) => `${name} 未记录当前版本;请先使用 \`xacpx plugin update ${name}\` 或显式选择版本。`,
1901
1914
  multiTargetNonInteractive: "检测到已安装插件;非交互模式请使用 `xacpx update --all` 或 `xacpx update <name>`。",
1902
1915
  selectionPrompt: "请选择要更新的项目(数字,逗号分隔,a=全部,回车取消):",
1903
1916
  selectionInvalid: (part) => `无效选择:${part}`,
@@ -1962,6 +1975,8 @@ var init_plugin_cli2 = __esm(() => {
1962
1975
  noPlugins: "还没有安装插件。",
1963
1976
  pluginListHeader: "插件:",
1964
1977
  unrecognizedArgs: (args) => `未识别的参数:${args}`,
1978
+ pluginSpecHasDoubleQuote: (spec) => `非法插件 spec ${spec}:npm 包 spec 不允许包含双引号 (")。`,
1979
+ pluginSpecHasPercentOnWindows: (spec) => `非法插件 spec ${spec}:Windows 上 cmd.exe 会展开 %,无法安全传递。请改用 npm 直接安装该包。`,
1965
1980
  pluginInstallFailed: (packageSpec, error) => `插件 ${packageSpec} 安装失败:${error}`,
1966
1981
  pluginValidateFailed: (recordedName, error) => `插件 ${recordedName} 校验失败:${error}`,
1967
1982
  pluginInstalled: (recordedName) => `插件 ${recordedName} 已安装`,
@@ -2073,8 +2088,6 @@ var init_weixin2 = __esm(() => {
2073
2088
  debugEnabled: "Debug 模式已开启",
2074
2089
  debugDisabled: "Debug 模式已关闭",
2075
2090
  sessionCleared: "✅ 会话已清除,重新开始对话",
2076
- noAccountsLoggedIn: "当前没有已登录的账号",
2077
- logoutSuccess: "✅ 已退出登录,清除所有账号凭证",
2078
2091
  commandFailed: (detail) => `❌ 指令执行失败: ${detail}`
2079
2092
  };
2080
2093
  });
@@ -2112,6 +2125,7 @@ var init_misc2 = __esm(() => {
2112
2125
  quotaOverflowSummary: (count) => `(因消息次数限制省略 ${count} 条进度,请继续查看下方最终结果)`,
2113
2126
  finalHeadsUp: (total, sentSoFar, remaining) => `—
2114
2127
  \uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`,
2128
+ finalAllParked: (count) => `\uD83D\uDCC4 已达消息上限:结果共 ${count} 段已暂存。回复 /jx 接收。`,
2115
2129
  quotedMessagePrefix: (parts) => `[引用: ${parts}]`,
2116
2130
  scheduledTaskFailed: (message) => `定时任务执行失败:${message}`,
2117
2131
  orchestrationTaskCompleted: (taskId, workerSession, result) => `委派任务「${taskId}」已完成
@@ -2140,6 +2154,8 @@ var init_misc2 = __esm(() => {
2140
2154
  delegateQPackageInstr3: "不要直接把 human 原话转发给 worker",
2141
2155
  commandAccessDeniedSuffix: " 仅限群创建者/频道 owner 使用。",
2142
2156
  commandAccessDeniedHint: "如果需要执行控制类操作,请由 owner 在群内发送,或改用私聊。",
2157
+ commandAccessDeniedChatTypeMissingSuffix: " 已被拦截:该频道未上报会话类型(直聊/群聊),控制类命令在此暂不可用。",
2158
+ commandAccessDeniedChatTypeMissingHint: "只读命令与普通对话不受影响。这是频道元数据问题,请升级或反馈该频道插件。",
2143
2159
  commandLabelThisMessage: "该消息",
2144
2160
  sessionResetNoCurrentSession: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。",
2145
2161
  sessionResetFailed: (alias) => `会话「${alias}」重置失败。
@@ -28,8 +28,11 @@ export declare class ScheduledTaskService {
28
28
  private readonly claimedInThisSession;
29
29
  constructor(state: AppState, stateStore: Pick<StateStore, "save">, options?: ScheduledTaskServiceOptions);
30
30
  createTask(input: CreateScheduledTaskInput): Promise<ScheduledTaskRecord>;
31
- listPending(): ScheduledTaskRecord[];
32
- cancelPending(inputId: string): Promise<boolean>;
31
+ listPending(chatKey: string): ScheduledTaskRecord[];
32
+ listPendingAllChats(): ScheduledTaskRecord[];
33
+ cancelPending(inputId: string, chatKey: string): Promise<boolean>;
34
+ cancelPendingAnyChat(inputId: string): Promise<boolean>;
35
+ private cancelPendingWhere;
33
36
  markStartupMissed(): Promise<void>;
34
37
  claimDueTasks(): Promise<ScheduledTaskRecord[]>;
35
38
  markExecuted(id: string): Promise<void>;
@@ -67,9 +67,13 @@ export declare class SessionService {
67
67
  createSession(alias: string, agent: string, workspace: string): Promise<ResolvedSession>;
68
68
  /**
69
69
  * All currently-known logical sessions resolved to transport sessions, deduped by
70
- * transport session. Sessions whose agent or workspace is no longer registered are
71
- * skipped (toResolvedSession would throw). Used by shutdown cleanup to reap warm
72
- * acpx queue owners; never throws.
70
+ * the composite identity acpx keys its session records on (agent + agent command +
71
+ * cwd + transport session name). Two aliases sharing a transport-session *name*
72
+ * but differing in agent/cwd (possible via /session attach) resolve to different
73
+ * acpx records with their own warm queue owners, so both must survive. Sessions
74
+ * whose agent or workspace is no longer registered are skipped (toResolvedSession
75
+ * would throw). Used by startup/shutdown cleanup to reap warm acpx queue owners;
76
+ * never throws.
73
77
  */
74
78
  listAllResolvedSessions(): ResolvedSession[];
75
79
  resolveSession(alias: string, agent: string, workspace: string, transportSession: string): ResolvedSession;
@@ -1,8 +1,75 @@
1
1
  import { type AppState } from "./types";
2
- export declare function parseState(raw: unknown, path: string): AppState;
2
+ /**
3
+ * Format version written on every save. The parser tolerates files without it
4
+ * (older releases) and ignores it on load; no migration chain exists yet.
5
+ */
6
+ export declare const STATE_FILE_VERSION = 1;
7
+ /** A record or section that was skipped/repaired while loading state.json. */
8
+ export interface StateLoadDroppedRecord {
9
+ section: string;
10
+ key: string;
11
+ reason: string;
12
+ }
13
+ export interface StateLoadReport {
14
+ dropped: StateLoadDroppedRecord[];
15
+ /** Backup copy of the original file, written because records were dropped. */
16
+ quarantinePath?: string;
17
+ /** Unreadable original renamed aside (whole-file JSON corruption). */
18
+ corruptPath?: string;
19
+ /** Best-effort backup/rename failure; load still returned the cleaned state. */
20
+ backupError?: string;
21
+ }
22
+ /**
23
+ * Lenient state parser: a malformed record (or wrong-typed section) is skipped
24
+ * and collected in `dropped` instead of throwing, so one bad record can never
25
+ * brick daemon startup. The per-record shape checks themselves stay strict — an
26
+ * invalid record is quarantined, it never flows into dispatch logic. Only a
27
+ * non-object top level still throws (StateStore.load treats that as a corrupt
28
+ * file and renames it aside).
29
+ */
30
+ export declare function parseState(raw: unknown, path: string, dropped?: StateLoadDroppedRecord[]): AppState;
31
+ export interface StateStoreOptions {
32
+ /** Injectable clock used for quarantine/corrupt backup file names. */
33
+ now?: () => Date;
34
+ /**
35
+ * Injectable backup writer (tests simulate backup failures). May return the
36
+ * path actually written when it differs from the requested one (the default
37
+ * writer suffix-retries instead of overwriting an existing backup).
38
+ */
39
+ writeBackup?: (targetPath: string, content: string) => Promise<string | void>;
40
+ }
41
+ /** Result of a side-effect-free {@link StateStore.inspect}. */
42
+ export interface StateLoadInspection {
43
+ state: AppState;
44
+ /** Null when the file is fully valid (or missing/empty). */
45
+ report: StateLoadReport | null;
46
+ }
3
47
  export declare class StateStore {
4
48
  private readonly path;
5
- constructor(path: string);
49
+ private readonly options;
50
+ private loadReport;
51
+ constructor(path: string, options?: StateStoreOptions);
52
+ /**
53
+ * Report of the most recent load(): null when the file was fully valid (or
54
+ * missing/empty), otherwise the dropped/repaired records plus the quarantine
55
+ * or corrupt backup path. Callers log/print this so silent repair is visible.
56
+ */
57
+ get lastLoadReport(): StateLoadReport | null;
6
58
  load(): Promise<AppState>;
59
+ /**
60
+ * Side-effect-free variant of load() for diagnostic callers (doctor): parses
61
+ * and reports exactly what load() would drop/repair, but never writes a
62
+ * quarantine backup, never renames a corrupt file, and does not touch
63
+ * {@link lastLoadReport}.
64
+ */
65
+ inspect(): Promise<StateLoadInspection>;
66
+ private readAndParse;
7
67
  save(state: AppState): Promise<void>;
68
+ private fileTimestamp;
69
+ /**
70
+ * Whole-file corruption (JSON syntax error / non-object top level): rename
71
+ * the file aside — not copy — so the next save does not fight the corrupt
72
+ * bytes, then start from an empty state.
73
+ */
74
+ private recoverFromCorruptFile;
8
75
  }
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Runs `fn` while holding the file's proper-lockfile lock, so a caller can make
3
+ * a whole read→modify→write span atomic with respect to other xacpx-aware
4
+ * processes (writePrivateFileAtomic alone only serializes the write).
5
+ *
6
+ * proper-lockfile is NOT reentrant: inside `fn`, write through the provided
7
+ * `writeLocked` (same atomic semantics, no re-lock) — never call
8
+ * writePrivateFileAtomic on the same path or it will deadlock until the lock
9
+ * goes stale. `realpath: false` keeps locking working for a not-yet-existing
10
+ * target file (the lock lives at `<path>.lock`).
11
+ */
12
+ export declare function withPrivateFileLock<T>(path: string, fn: (writeLocked: (content: string) => Promise<void>) => Promise<T>): Promise<T>;
1
13
  export declare function writePrivateFileAtomic(path: string, content: string): Promise<void>;
2
14
  /**
3
15
  * Synchronous private-file write for hot-path callers that cannot await
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",