@yottameta/yotta-memory 0.24.0 → 0.24.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.24.3 (2026-11-14)
4
+
5
+ - **CI 精度回归**:将命中打点后的 mtime 断言收紧到 Node 原生精度边界内(纳秒偏差 < 5µs)并保留可见毫秒不变;避免 Linux 上 `utimesSync` 的亚微秒浮点误差把正确行为误判为失败。
6
+
7
+ ## v0.24.2 (2026-11-14)
8
+
9
+ - **跨平台修复**:Windows profile 绝对路径(如 `C:\Users\...\serve-profile.json`)在 Linux CI / 非 Windows 环境生成任务 XML 时不再被 `path.resolve()` 误拼成仓库内相对路径;新增 host-independent profile 路径解析并保留 XML 回归。
10
+
11
+ ## v0.24.1 (2026-11-14)
12
+
13
+ - **自启服务 profile**:`lan enable` 在目标用户上下文解析并写入 `serve-profile.json`(memoryHome / runtimeHome / configDir / host / port / userId),Windows 任务 XML、Startup 兜底、systemd unit 与 crontab 均显式引用该 profile,不再依赖 S4U 启动环境的 `os.homedir()`。
14
+ - **serve fail-closed**:新增 `serve --profile <文件>`;启动时校验 profile schema、绝对路径、memoryHome / runtimeHome 是否存在、userId 是否匹配,并原子覆盖进程环境。缺文件、空库、错误用户或非法路径均拒绝启动并给出中文提示。
15
+ - **状态与迁移**:`lan status` 显示 profile 的 memoryHome / runtimeHome,与当前配置比较漂移;旧任务缺少 `--profile` 时提示重新执行 `lan enable`。重新执行 `lan enable` 即可迁移旧任务。
16
+ - **验证**:新增 Windows task / Startup / Linux unit / crontab profile 回归,以及 `serve --profile` 的路径固化、缺库、错用户、CLI 缺文件用例;全量测试覆盖正常与失败路径。
17
+
3
18
  ## v0.24.0 (2026-11-11)
4
19
 
5
20
  - **备份可靠性**:`backup status` 改为按 manifest `created` 解析时间排序,超过当前时间 15 分钟的备份标记 `future_timestamp` 并排除;manifest 缺失 / 不完整 / created 无效不参与健康判定;新增 `latest_valid` / `config_last_success` / `effective_last_success` / `source` / `anomalies`,`ensure-daily` 与默认备份体检复用同一选择逻辑。
package/README.md CHANGED
@@ -33,6 +33,7 @@
33
33
 
34
34
  > 📖 The user-facing operations manual lives in [USER_GUIDE.md](USER_GUIDE.md).
35
35
  > 🆕 **v0.24.0 (structural capacity)**: `backup status` now selects valid manifest times and isolates future-dated anomalies; `index --stats` adds read-only field/byte analysis; index v5 uses gzip year shards while remaining v4-compatible; new writes use `yyyy/mm/dd` buckets with `layout plan/apply/rollback`; `doctor` adds four type-pressure indicators; `scan` supports exact rule + file + line-hash + expiry policies, while `--policy-candidates` only generates candidates.
36
+ > 🆕 **v0.24.1 (autostart profile)**: `lan enable` writes a per-user `serve-profile.json`; Windows task XML, Startup fallback, systemd and crontab reference that absolute profile. `serve --profile <file>` pins memoryHome / runtimeHome / configDir and fails closed when paths or the Windows user do not match, preventing S4U / no-profile startup from landing on `C:\Users\Default\.yottamemory` and returning remote MCP 401.
36
37
  > 🆕 **v0.23.2 (ClawHub storefront)**: the ClawHub listing now ships an English-first bilingual summary and description (a dedicated `clawhub-dist/yotta-memory` publish copy), and the README gained the three usage cases above. Commands, options, output and the memory store format are unchanged.
37
38
  > 🆕 **v0.18.1 (distillation provenance + absolute dates + consolidation markers + rule-promotion suggestions + write discipline)**: `distill` now emits typed extraction lists (event / lesson / todo / growth / boundary-rule / other) with per-item `[source: file#L<a>-L<b>]` anchors, measured quality metrics and a skipped/boundary list, plus structured `--json`; `consolidate` rewrites relative dates ("yesterday / last week / this month / this year") into absolute dates based on each entry's `created` date and appends a consolidation marker to archived copies (`--undo` strips it); `maintain --rules` prints read-only "repeat pitfalls → promote to a rule" suggestions (default threshold 3, never writes BOUND automatically); the Memory Doctrine gained a 6-level conflict authority order and three write-discipline rules.
38
39
 
@@ -318,7 +319,8 @@ Optional post-upgrade self-check: `yotta-memory config get` (confirm `memory_hom
318
319
  | `yotta-memory iam <id> [--name <name>] [--user <user>] [--relationship <rel>] [--force]` | Register this agent's unique identity and auto-write the self profile (`agents.json`, ID must be unique) |
319
320
  | `yotta-memory token new --agent <id> [--force]` / `token list` / `token revoke --agent <id>` | Create / list / revoke access tokens for agents (registered at `.server/tokens.json`) |
320
321
  | `yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio]` | Start the MCP memory engine (streamable HTTP LAN / --stdio local zero-process mode; Bearer token + X-Agent-Id + X-Agent-Key auth) |
321
- | `yotta-memory lan enable [--onstart] / disable / status` | Autostart management (Windows: scheduled task with an S4U principal — runs silently in the background with no console window; `--onstart` starts at boot without a logon session, registration needs admin; non-admin auto-degrades to user-level silent Startup; Linux: systemd user unit, falls back to user crontab @reboot) |
322
+ | `yotta-memory lan enable [--onstart] / disable / status` | Autostart management (Windows: scheduled task with an S4U principal — runs silently in the background with no console window; `--onstart` starts at boot without a logon session, registration needs admin; non-admin auto-degrades to user-level silent Startup; Linux: systemd user unit, falls back to user crontab @reboot). v0.24.1 persists a per-user `serve-profile.json`; `lan status` reports drift and old tasks lacking `--profile` |
323
+ | `yotta-memory serve --profile <file>` | Read the per-user service profile, pin memory/runtime/config roots, validate absolute paths and the Windows user, and fail closed before opening the MCP port when a path is missing |
322
324
  | `yotta-memory maintain [--dry-run] [--apply] [--purge] [--threshold N] [--age N] [--dedup] [--dedup --apply] [--merge A,B]` | Self-organization: archive / forget candidates / confidence-scored dedup / auto-merge high-confidence groups; dry-run by default; `--dedup` is mutually exclusive with archiving |
323
325
  | `yotta-memory consolidate [--min-age N] [--min-idle N] [--max-utility N] [--min-group N] [--period N] [--type T] [--model <cmd>] [--apply] [--undo <batch>] [--batches]` | Periodic-summary compression (v0.10.0): group old idle low-value memories into one traceable summary and archive the originals; dry-run by default; `--undo <batch>` rolls a batch back; `--batches` lists batches |
324
326
  | `yotta-memory feedback <file> --useful|--useless [--reason <r>] [--undo]` | Usage feedback (useful/useless adjusts weight / confidence / feedback_net; --undo rolls back the last one) |
package/README.zh-CN.md CHANGED
@@ -23,6 +23,7 @@
23
23
 
24
24
  > 📖 面向用户的操作手册见 [USER_GUIDE.md](USER_GUIDE.md)。
25
25
  > 🆕 **v0.24.0(结构容量整改)**:`backup status` 按 manifest 时间选择有效备份并隔离未来时间异常;新增只读 `index --stats`,索引 v5 使用年份 gzip 分片并兼容 v4;新写入进入 `yyyy/mm/dd` 日级桶,`layout plan/apply/rollback` 支持可回滚迁移;`doctor` 增加类型压力四指标;`scan` 支持精确文件 + 规则 + 行哈希 + 有效期的例外策略,`--policy-candidates` 只生成候选、不自动启用。
26
+ > 🆕 **v0.24.1(自启服务 profile)**:`lan enable` 在目标用户上下文生成 `serve-profile.json`;Windows 计划任务、Startup 兜底、systemd 与 crontab 都引用该绝对 profile。`serve --profile <文件>` 固定 memoryHome / runtimeHome / configDir,并在路径缺失或 Windows 用户不匹配时 fail-closed,避免 S4U / 无用户环境启动落到 `C:\Users\Default\.yottamemory` 空库后返回远端 MCP 401。
26
27
  > 🆕 **v0.23.2(ClawHub 门面)**:ClawHub 上的技能卡改为英文优先的双语摘要与描述(新增 `clawhub-dist/yotta-memory` 专属发布副本),README 首屏新增上面三条使用案例;命令、参数、输出与记忆库格式全部不变。
27
28
  > 🆕 **v0.20.0(上下文分页 + 画像驱逐修复)**:`memory.hook` 的 PREF 驱逐空转已修复——hook / paging 决策生效时,画像段落按实时可读条目渲染并套用同一套过滤;自我接入档案不进候选、不可驱逐。新增 `context.paging`:仅在显式 `--budget` 时调用,hook 先过滤、paging 再排序与分页;`drop` 始终安全应用,`order` 仅在 `complete:true` 且未截断时应用。`context --json` 新增 `paging` 块,`--explain` 增加 `[paging]` 轨迹。配置与协议见 `references/provider-protocol.md`。
28
29
  > 🆕 **v0.18.1(蒸馏溯源 + 日期绝对化 + 巩固标记 + 规则晋升建议 + 写入纪律)**:`distill` 新增分类型提取清单(事件 / 教训 / 待办 / 成长 / 规则边界 / 其他)+ 逐条 `[溯源: 文件#L<a>-L<b>]` + 实测质量指标 + 跳过与边界,`--json` 输出结构化报告;`consolidate` 摘要按条目 `created` 把「昨天 / 上周 / 本月 / 今年」写成绝对日期,归档副本写入巩固标记(`--undo` 剥离还原);`maintain --rules` 只读给出「重复踩坑 → 建议升级为规则」(默认阈值 3,不自动写 BOUND);记忆守则新增冲突权威顺序 6 层与写入纪律 3 条。
@@ -360,7 +361,8 @@ bash install.sh --agent <智能体名称>
360
361
  | `yotta-memory iam <id> [--name <显示名>] [--user <用户名>] [--relationship <关系>] [--force]` | 登记本智能体唯一身份并自动落自我档案(`agents.json`,ID 必须唯一;可选扩展显示名 / 用户 / 关系)|
361
362
  | `yotta-memory token new --agent <id> [--force]` / `token list` / `token revoke --agent <id>` | 为智能体生成 / 列出 / 吊销访问 token(登记于记忆库 `.server/tokens.json`;同 ID 已被其它来源占用需 `--force` 覆盖,防不同智能体合流)|
362
363
  | `yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio]` | 启动 MCP 记忆引擎(streamable HTTP 局域网 / --stdio 本地零进程模式;Bearer token + X-Agent-Id + X-Agent-Key 鉴权)|
363
- | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows:计划任务 S4U 主体后台静默、不弹窗口;默认登录自启、`--onstart` 开机即启(无登录会话也可启动,注册需管理员),非管理员自动降级用户级 Startup 静默自启;Linux:systemd 用户单元,不可用时自动降级用户 crontab @reboot)|
364
+ | `yotta-memory lan enable [--onstart] / disable / status` | 开机自启管理(Windows:计划任务 S4U 主体后台静默、不弹窗口;默认登录自启、`--onstart` 开机即启(无登录会话也可启动,注册需管理员),非管理员自动降级用户级 Startup 静默自启;Linux:systemd 用户单元,不可用时自动降级用户 crontab @reboot)。v0.24.1 生成每用户 `serve-profile.json`;`lan status` 检查路径漂移并提示缺少 `--profile` 的旧任务 |
365
+ | `yotta-memory serve --profile <文件>` | 读取每用户服务 profile,固定记忆库 / 运行时 / 配置目录;路径缺失、非法或 Windows 用户不匹配时在监听端口前 fail-closed |
364
366
  | `yotta-memory maintain [--dry-run] [--apply] [--purge] [--threshold N] [--age N] [--dedup] [--dedup --apply] [--merge A,B]` / `maintain --capacity [--json]` | 记忆自组织:归档 / 遗忘候选 / 置信度查重 / 自动合并高置信组;默认 dry-run;`--dedup` 与归档互斥。`--capacity` 只读报告容量水位、LRU / LFU 淘汰候选与晋升建议(冷却期与常青条目豁免)|
365
367
  | `yotta-memory consolidate [--propose] [--min-age N] [--min-idle N] [--max-utility N] [--min-group N] [--period N] [--type T] [--model <cmd>] [--apply [--yes]] [--undo <batch>] [--batches]` | 周期摘要压缩:同主题旧记忆 → 带溯源摘要 + 原文归档;默认等价 `--propose`(结构化报告,不写盘);`--apply` 需确认(非交互必须 `--yes`);`--undo <batch>` 回滚批次;`--batches` 查批次 |
366
368
  | `yotta-memory feedback <文件|主题> --useful|--useless [--reason <原因>] [--undo]` | 使用反馈(useful/useless 调整 weight / confidence / feedback_net;`--undo` 回滚最近一次)|
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: yotta-memory
3
3
  description: 元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/可回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆、画像、开工上下文、长期理解摘要、近期走廊、会话闭环、记忆守则、profile、context、越用越懂、语义检索、反馈、维护、蒸馏、feedback、maintain、distill、explain、自我学习、自我进化、自我提升、查看平台分页、recall 候选预过滤、任务相关记忆、--focus、--embedding、压缩遗忘、consolidate、周期摘要、自动合并、分类型衰减、回滚、备份、backup、防误删、doctor、事务快照、基线探针、恢复演练、记忆库安全扫描、scan、命中打点、容量水位、--capacity、--propose、上下文压缩审计、context --audit
4
- version: 0.24.0
4
+ version: 0.24.3
5
5
  license: MIT
6
6
  ---
7
7
 
@@ -14,6 +14,10 @@ license: MIT
14
14
  新写入进入 `yyyy/mm/dd` 日级桶,`layout plan/apply/rollback` 提供可预演、可回滚迁移;
15
15
  `doctor` 增加类型压力四指标(保留 7 天观察窗);`scan` 支持精确文件 + 规则 + 行哈希 +
16
16
  有效期的例外策略,`--policy-candidates` 只生成候选、不自动启用。
17
+ - **自启服务 profile(v0.24.1)**:`lan enable` 在目标用户上下文生成
18
+ `serve-profile.json`,Windows 计划任务、Startup 兜底、systemd 与 crontab
19
+ 均显式引用该 profile;`serve --profile <文件>` 固化 memory_home / runtime_home /
20
+ config_dir,避免 S4U 或无用户环境启动时 `os.homedir()` 漂移为空库并导致远端 401。
17
21
 
18
22
  ## 这是什么
19
23
 
package/USER_GUIDE.md CHANGED
@@ -327,6 +327,21 @@ yotta-memory recall <关键词> --agent <id> --agent-key-file "<AI_HOME>/.yotta-
327
327
  - `doctor --json` 的 `checks.scale` 新增 `index_logical_bytes` / `index_shards` / `index_format` 与 `type_pressure`(总数 / 180 天陈旧 / 归档候选 / 30 天增长);类型阈值按计划保留观察窗。
328
328
  - `scan` 精确例外:`security/scan-policy.json` 中每条必须同时匹配规则、文件、整行 SHA-256 与未过期的 `review_by`;内容变化或过期自动恢复命中。`scan --no-policy` 做全量复核;`scan --policy-candidates --out security/scan-policy.candidates.json` 只生成候选,不自动启用。
329
329
 
330
+ ## 3.12 每用户自启 profile(v0.24.1)
331
+
332
+ - `yotta-memory lan enable` 在目标用户上下文解析 `memoryHome`、`runtimeHome`、
333
+ `configDir`、`host`、`port` 和 `userId`,写到该用户配置目录下的
334
+ `serve-profile.json`。不同用户的路径互不影响,不是代码或全机器共享常量。
335
+ - Windows task XML、Startup 兜底、systemd unit 与 crontab 都引用该绝对 profile
336
+ 路径。服务由 S4U 或无用户环境启动时,不再依赖 `os.homedir()`。
337
+ - `yotta-memory serve --profile <文件>` 在监听端口前校验 profile schema、
338
+ 绝对路径、memoryHome / runtimeHome 是否存在以及 Windows 用户是否匹配;
339
+ 校验失败直接退出,不创建 `C:\Users\Default\.yottamemory` 空库。
340
+ - `yotta-memory lan status` 显示 profile 中的记忆库和运行时路径,并与当前
341
+ 设置比较漂移;旧任务命令缺少 `--profile` 时提示重新执行 `lan enable`。
342
+ - 修改 `memory_home`、配置目录或 runtime 路径后,应重新执行
343
+ `yotta-memory lan enable --onstart` 更新 profile 和自启任务。
344
+
330
345
  ## 4. 便携记忆盘 · 记忆引擎主机篇
331
346
 
332
347
  场景:记忆放在一台主机上(Linux / Windows 均可),本机直接 CLI 读写;局域网内其它主机上的 AI 智能体经 MCP 远程接入。引擎主机只需装 CLI,不需要装任何 AI 智能体。
@@ -26,8 +26,8 @@ const zlib = require('zlib');
26
26
  const child_process = require('child_process');
27
27
  const { AsyncLocalStorage } = require('async_hooks');
28
28
 
29
- const VERSION = '0.24.0';
30
- const CLI_VALUE_OPTS = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir', '--year', '--evalset', '--k', '--seed', '--bootstrap', '--gate', '--against', '--template', '--path', '--from', '--from-plan', '--batch']);
29
+ const VERSION = '0.24.3';
30
+ const CLI_VALUE_OPTS = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir', '--year', '--evalset', '--k', '--seed', '--bootstrap', '--gate', '--against', '--template', '--path', '--from', '--from-plan', '--batch', '--profile']);
31
31
  const CLI_FLAG_OPTS = new Set(['--project', '--all', '--unsafe', '--no-auth', '--stdio', '--onstart', '--from-current', '--restart', '--force', '--attach', '--allow-same-volume', '--verify', '--no-hint', '--encrypt', '--no-encrypt', '--password-stdin', '--json', '--manual', '--skip-schedule', '--useful', '--useless', '--undo', '--dry-run', '--apply', '--purge', '--dedup', '--batches', '--propose', '--audit', '--capacity', '--rules', '--explain', '--semantic', '--runtime', '--ablate', '--timing', '--baseline', '--probe', '--quarantine', '--restore', '--no-usage', '--yes', '--keep-memories', '--keep-identity', '--stats', '--include-flat', '--no-policy', '--policy-candidates']);
32
32
 
33
33
  function helpOption(flag, arg, what, when, caution) {
@@ -316,8 +316,9 @@ const HELP_MODEL = [
316
316
  helpSub('status', 'status [--json]', '查看 current 与运行时状态', '排查版本漂移时', [helpOption('--json', '', '输出 JSON', '自动化读取运行时状态时', '')]),
317
317
  helpSub('list', 'list [--json]', '列出已安装运行时版本', '盘点可切换版本时', [helpOption('--json', '', '输出 JSON', '自动化盘点运行时版本时', '')]),
318
318
  ] },
319
- { name: 'serve', usage: 'serve [--stdio] [--tools core|full] [--host <地址>] [--port <端口>]', what: '启动 MCP 记忆引擎', when: '宿主需要以 MCP 方式访问元忆时', options: [
319
+ { name: 'serve', usage: 'serve [--stdio] [--profile <文件>] [--tools core|full] [--host <地址>] [--port <端口>]', what: '启动 MCP 记忆引擎', when: '宿主需要以 MCP 方式访问元忆时', options: [
320
320
  helpOption('--stdio', '', '用本地 stdio 模式启动', '本地宿主不需要 HTTP 服务时', ''),
321
+ helpOption('--profile', '<文件>', '读取每用户服务 profile', '自启任务需要固定记忆库与运行时路径时', 'profile 不存在、路径无效或用户不匹配时拒绝启动'),
321
322
  helpOption('--tools', 'core|full', '选择工具分组', '想减少工具常驻开销时', 'core 更省上下文,full 暴露全部工具'),
322
323
  helpOption('--host', '<地址>', '设置 HTTP 监听地址', '需要远端 MCP 接入时', '监听公网前必须配置鉴权和防火墙'),
323
324
  helpOption('--port', '<端口>', '设置 HTTP 监听端口', '默认端口被占用时', ''),
@@ -11623,6 +11624,7 @@ function cmdServe(opts) {
11623
11624
  server.listen(port, host, function () {
11624
11625
  console.log('yotta-memory 记忆引擎已启动(v' + VERSION + ')');
11625
11626
  console.log('URL: http://' + host + ':' + port + '/mcp');
11627
+ if (opts.profile) console.log('服务 profile: ' + path.resolve(String(opts.profile)));
11626
11628
  console.log('记忆库: ' + root);
11627
11629
  console.log('MCP 协议: 2026-07-28(modern,server/discover)/ 2025-11-25(legacy 握手兼容)');
11628
11630
  console.log('工具分组: ' + normalizeMcpToolProfile(opts.toolProfile));
@@ -11691,18 +11693,144 @@ const LAN_TASK_NAME = 'YottaMemoryServe';
11691
11693
  const LAN_GEN_MARKER = 'Generated by yotta-memory lan enable';
11692
11694
  const LAN_UNIT_NAME = 'yotta-memory-serve.service';
11693
11695
  const LAN_CRONTAB_MARKER = '#YTM_LAN:yotta-memory-serve';
11696
+ const SERVE_PROFILE_FILE = 'serve-profile.json';
11697
+ const SERVE_PROFILE_SCHEMA = 1;
11698
+
11699
+ function resolveProfilePath(value) {
11700
+ const text = String(value || '');
11701
+ if (!text) return '';
11702
+ if (path.isAbsolute(text)) return path.resolve(text);
11703
+ if (path.win32.isAbsolute(text)) return path.win32.normalize(text);
11704
+ return path.resolve(text);
11705
+ }
11706
+
11707
+ function lanServeProfilePath(opts) {
11708
+ opts = opts || {};
11709
+ if (opts.profilePath) return resolveProfilePath(opts.profilePath);
11710
+ if (process.env.YOTTA_LAN_PROFILE_FILE) return resolveProfilePath(process.env.YOTTA_LAN_PROFILE_FILE);
11711
+ const configDir = opts.configDir || process.env.YOTTA_MEMORY_CONFIG_DIR || path.dirname(configPath());
11712
+ return path.join(path.resolve(String(configDir)), SERVE_PROFILE_FILE);
11713
+ }
11714
+
11715
+ function lanCurrentUser(opts) {
11716
+ opts = opts || {};
11717
+ if (opts.currentUser) return String(opts.currentUser);
11718
+ if (lanPlatform() === 'win32') return lanWindowsTaskUser(opts);
11719
+ try {
11720
+ const info = os.userInfo();
11721
+ return String(info.username || '');
11722
+ } catch (e) {
11723
+ return '';
11724
+ }
11725
+ }
11726
+
11727
+ function sameLanUser(a, b) {
11728
+ const left = String(a || '').trim().toLowerCase();
11729
+ const right = String(b || '').trim().toLowerCase();
11730
+ return !left || !right || left === right;
11731
+ }
11732
+
11733
+ function lanWriteServeProfile(opts) {
11734
+ opts = opts || {};
11735
+ try {
11736
+ const memoryHome = path.resolve(String(opts.memoryHome || userRoot()));
11737
+ const runtimeHome = path.resolve(String(opts.runtimeHome || runtimeRoot()));
11738
+ const configDir = path.resolve(String(opts.configDir || process.env.YOTTA_MEMORY_CONFIG_DIR || path.dirname(configPath())));
11739
+ const profilePath = lanServeProfilePath(Object.assign({}, opts, { configDir: configDir }));
11740
+ if (!fs.existsSync(memoryHome) || !fs.statSync(memoryHome).isDirectory()) {
11741
+ return { error: true, text: '记忆库不存在或不是目录: ' + memoryHome + '。请先初始化或修正 memory_home。' };
11742
+ }
11743
+ const profile = {
11744
+ schemaVersion: SERVE_PROFILE_SCHEMA,
11745
+ generatedAt: new Date().toISOString(),
11746
+ userId: String(opts.userId || lanCurrentUser(opts)),
11747
+ memoryHome: memoryHome,
11748
+ runtimeHome: runtimeHome,
11749
+ configDir: configDir,
11750
+ host: String(opts.host || '0.0.0.0'),
11751
+ port: parseInt(opts.port, 10) || 8787,
11752
+ };
11753
+ fs.mkdirSync(path.dirname(profilePath), { recursive: true });
11754
+ atomicWriteFileSync(profilePath, JSON.stringify(profile, null, 2) + '\n', 'utf8');
11755
+ return { error: false, profilePath: profilePath, profile: profile };
11756
+ } catch (e) {
11757
+ return { error: true, text: '写入服务 profile 失败: ' + String((e && e.message) || e) };
11758
+ }
11759
+ }
11760
+
11761
+ function loadServeProfile(profilePath, opts) {
11762
+ opts = opts || {};
11763
+ const resolved = resolveProfilePath(profilePath);
11764
+ try {
11765
+ if (!profilePath) return { error: true, text: 'profile 路径不能为空。' };
11766
+ const profile = JSON.parse(String(fs.readFileSync(resolved, 'utf8')).replace(/^\uFEFF/, ''));
11767
+ if (!profile || profile.schemaVersion !== SERVE_PROFILE_SCHEMA) {
11768
+ return { error: true, text: 'profile schemaVersion 无效,需要 ' + SERVE_PROFILE_SCHEMA + '。' };
11769
+ }
11770
+ for (const key of ['memoryHome', 'runtimeHome', 'configDir']) {
11771
+ const value = String(profile[key] || '').trim();
11772
+ if (!value || !path.isAbsolute(value)) {
11773
+ return { error: true, text: 'profile 字段 ' + key + ' 必须是绝对路径。' };
11774
+ }
11775
+ }
11776
+ const memoryHome = path.resolve(String(profile.memoryHome));
11777
+ const runtimeHome = path.resolve(String(profile.runtimeHome));
11778
+ const configDir = path.resolve(String(profile.configDir));
11779
+ if (!fs.existsSync(memoryHome) || !fs.statSync(memoryHome).isDirectory()) {
11780
+ return { error: true, text: 'profile memoryHome 指定的记忆库不存在或不是目录: ' + memoryHome };
11781
+ }
11782
+ if (!fs.existsSync(runtimeHome) || !fs.statSync(runtimeHome).isDirectory()) {
11783
+ return { error: true, text: 'profile runtimeHome 指定的运行时目录不存在或不是目录: ' + runtimeHome };
11784
+ }
11785
+ const currentUser = opts.currentUser || lanCurrentUser(opts);
11786
+ if (!sameLanUser(profile.userId, currentUser)) {
11787
+ return { error: true, text: 'profile userId 与当前用户不匹配: ' + String(profile.userId || '(empty)') + ' != ' + String(currentUser || '(empty)') };
11788
+ }
11789
+ return {
11790
+ error: false,
11791
+ profilePath: resolved,
11792
+ profile: Object.assign({}, profile, {
11793
+ memoryHome: memoryHome,
11794
+ runtimeHome: runtimeHome,
11795
+ configDir: configDir,
11796
+ }),
11797
+ };
11798
+ } catch (e) {
11799
+ return { error: true, text: '无法读取服务 profile ' + resolved + ': ' + String((e && e.message) || e) };
11800
+ }
11801
+ }
11802
+
11803
+ function applyServeProfile(profilePath, opts) {
11804
+ const loaded = loadServeProfile(profilePath, opts);
11805
+ if (loaded.error) return loaded;
11806
+ process.env.YOTTA_MEMORY_HOME = loaded.profile.memoryHome;
11807
+ process.env.YOTTA_MEMORY_RUNTIME_HOME = loaded.profile.runtimeHome;
11808
+ process.env.YOTTA_MEMORY_CONFIG_DIR = loaded.profile.configDir;
11809
+ return loaded;
11810
+ }
11811
+
11812
+ function serveProfileDrift(profile) {
11813
+ if (!profile) return [];
11814
+ const drift = [];
11815
+ if (path.resolve(String(profile.memoryHome || '')) !== path.resolve(userRoot())) drift.push('memoryHome');
11816
+ if (path.resolve(String(profile.runtimeHome || '')) !== path.resolve(runtimeRoot())) drift.push('runtimeHome');
11817
+ if (path.resolve(String(profile.configDir || '')) !== path.resolve(path.dirname(configPath()))) drift.push('configDir');
11818
+ return drift;
11819
+ }
11820
+
11694
11821
  function lanTaskRunCmd(opts) {
11695
11822
  opts = opts || {};
11696
11823
  const host = opts.host || '0.0.0.0';
11697
11824
  const port = opts.port || 8787;
11825
+ const profilePath = lanServeProfilePath(opts);
11698
11826
  // schtasks /tr 不接受多余内嵌引号:路径无空格不加引号,含空格/引号用 \" 转义(0.5.2 修复)
11699
11827
  const q = (p) => /[\s"]/.test(p) ? '\\"' + p.replace(/"/g, '\\"') + '\\"' : p;
11700
11828
  const prefix = lanElectronHost(opts) ? 'set "ELECTRON_RUN_AS_NODE=1" && ' : '';
11701
- return prefix + q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port;
11829
+ return prefix + q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --profile ' + q(profilePath) + ' --host ' + host + ' --port ' + port;
11702
11830
  }
11703
11831
 
11704
11832
  function lanServeArgs(opts) {
11705
- return ['serve', '--host', String(opts.host || '0.0.0.0'), '--port', String(opts.port || 8787)];
11833
+ return ['serve', '--profile', lanServeProfilePath(opts), '--host', String(opts.host || '0.0.0.0'), '--port', String(opts.port || 8787)];
11706
11834
  }
11707
11835
  // generic spawn wrapper: YOTTA_LAN_*_BIN may point to a .js stub (testing/advanced use;
11708
11836
  // on Windows run it with the current node interpreter automatically)
@@ -11773,6 +11901,7 @@ function lanAutostartCmdContent(opts) {
11773
11901
  opts = opts || {};
11774
11902
  const host = opts.host || '0.0.0.0';
11775
11903
  const port = opts.port || 8787;
11904
+ const profilePath = lanServeProfilePath(opts);
11776
11905
  const q = (p) => '"' + String(p).replace(/"/g, '""') + '"';
11777
11906
  // 生成文件用纯 ASCII(避免 cmd/VBS 按系统代码页读取中文注释乱码;VBS 自愈内联同一内容)
11778
11907
  // v0.22.5: Electron 宿主下需先设 ELECTRON_RUN_AS_NODE=1,否则拉起桌面程序本体而非 Node。
@@ -11780,7 +11909,7 @@ function lanAutostartCmdContent(opts) {
11780
11909
  + 'chcp 65001 >nul\r\n'
11781
11910
  + 'rem ' + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable (English only, keep ASCII)\r\n'
11782
11911
  + (lanElectronHost(opts) ? 'set "ELECTRON_RUN_AS_NODE=1"\r\n' : '')
11783
- + q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port + ' >> ' + q(lanLogPath(opts)) + ' 2>&1\r\n';
11912
+ + q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --profile ' + q(profilePath) + ' --host ' + host + ' --port ' + port + ' >> ' + q(lanLogPath(opts)) + ' 2>&1\r\n';
11784
11913
  }
11785
11914
  function lanVbsContent(opts) {
11786
11915
  // v0.6.3 自愈:VBS 内联 autostart.cmd 内容,启动时若 .cmd 缺失/被清理即就地重建,
@@ -11893,6 +12022,7 @@ function lanWindowsTaskXml(opts) {
11893
12022
  const nodePath = opts.nodePath || process.execPath;
11894
12023
  const scriptPath = opts.scriptPath || runtimeManagedScript();
11895
12024
  const logPath = opts.logPath || lanLogPath(opts);
12025
+ const profilePath = lanServeProfilePath(opts);
11896
12026
  const userId = opts.userId || '';
11897
12027
  const cmdPath = opts.cmdPath || process.env.ComSpec || 'cmd.exe';
11898
12028
  // cmd /c 外层引号:命令首段带引号时必须用 ""..." 包裹(cmd 的 /c 引号规则),
@@ -11902,7 +12032,7 @@ function lanWindowsTaskXml(opts) {
11902
12032
  // v0.22.5: Electron 宿主(YottaCode shim)下先 set ELECTRON_RUN_AS_NODE=1,
11903
12033
  // 否则任务会拉起桌面程序本体(GUI)而不是 Node 服务。
11904
12034
  const envPrefix = lanElectronHost(opts) ? 'set "ELECTRON_RUN_AS_NODE=1" &amp;&amp; ' : '';
11905
- const inner = envPrefix + '"' + xmlEscape(nodePath) + '" "' + xmlEscape(scriptPath) + '" serve --host ' + host + ' --port ' + port
12035
+ const inner = envPrefix + '"' + xmlEscape(nodePath) + '" "' + xmlEscape(scriptPath) + '" serve --profile "' + xmlEscape(profilePath) + '" --host ' + host + ' --port ' + port
11906
12036
  + ' >> "' + xmlEscape(logPath) + '" 2>&amp;1';
11907
12037
  const triggerXml = onstart
11908
12038
  ? '<BootTrigger><Enabled>true</Enabled></BootTrigger>'
@@ -11937,6 +12067,8 @@ function lanWindowsTaskXml(opts) {
11937
12067
  }
11938
12068
  function lanWindowsEnableCore(opts) {
11939
12069
  opts = opts || {};
12070
+ const profileResult = lanWriteServeProfile(opts);
12071
+ if (profileResult.error) return { error: true, phase: 'profile', text: profileResult.text };
11940
12072
  const trigger = opts.onstart ? 'onstart' : 'onlogon';
11941
12073
  const host = opts.host || '0.0.0.0';
11942
12074
  const port = opts.port || 8787;
@@ -11944,7 +12076,7 @@ function lanWindowsEnableCore(opts) {
11944
12076
  const scriptPath = opts.scriptPath || runtimeManagedScript();
11945
12077
  const logPath = lanLogPath(opts);
11946
12078
  const userId = lanWindowsTaskUser(opts);
11947
- const xml = lanWindowsTaskXml({ trigger: trigger, host: host, port: port, nodePath: nodePath, scriptPath: scriptPath, logPath: logPath, userId: userId, electronHost: opts.electronHost });
12079
+ const xml = lanWindowsTaskXml({ trigger: trigger, host: host, port: port, nodePath: nodePath, scriptPath: scriptPath, logPath: logPath, profilePath: profileResult.profilePath, userId: userId, electronHost: opts.electronHost });
11948
12080
  const xmlDir = opts.xmlDir || process.env.YOTTA_LAN_TASK_XML_DIR || os.tmpdir();
11949
12081
  const xmlPath = opts.xmlPath || path.join(xmlDir, LAN_TASK_NAME + '-task-' + process.pid + '-' + Date.now() + '.xml');
11950
12082
  let failure = null;
@@ -11962,7 +12094,7 @@ function lanWindowsEnableCore(opts) {
11962
12094
  if (!failure) {
11963
12095
  let removedStartup = [];
11964
12096
  try { removedStartup = lanRemoveStartupFiles(opts); } catch (e) { removedStartup = []; }
11965
- return { error: false, mode: 'task', taskName: LAN_TASK_NAME, trigger: trigger, logPath: logPath, removedStartup: removedStartup };
12097
+ return { error: false, mode: 'task', taskName: LAN_TASK_NAME, trigger: trigger, logPath: logPath, profilePath: profileResult.profilePath, removedStartup: removedStartup };
11966
12098
  }
11967
12099
  try {
11968
12100
  lanInstallStartup(opts);
@@ -11978,6 +12110,7 @@ function lanWindowsEnableCore(opts) {
11978
12110
  vbsPath: lanVbsPath(opts),
11979
12111
  cmdPath: lanAutostartCmdPath(opts),
11980
12112
  logPath: logPath,
12113
+ profilePath: profileResult.profilePath,
11981
12114
  };
11982
12115
  }
11983
12116
 
@@ -11992,7 +12125,9 @@ function lanLinuxUnitPath() { return path.join(lanLinuxSystemdUserDir(), LAN_UNI
11992
12125
  function lanLinuxSystemctlBin() { return process.env.YOTTA_LAN_SYSTEMCTL_BIN || 'systemctl'; }
11993
12126
  function lanLinuxLoginctlBin() { return process.env.YOTTA_LAN_LOGINCTL_BIN || 'loginctl'; }
11994
12127
  function lanLinuxExecStart(opts) {
11995
- return [systemdEscapeArg(process.execPath), systemdEscapeArg(runtimeManagedScript())].concat(lanServeArgs(opts)).join(' ');
12128
+ return [systemdEscapeArg(process.execPath), systemdEscapeArg(runtimeManagedScript())]
12129
+ .concat(lanServeArgs(opts).map(systemdEscapeArg))
12130
+ .join(' ');
11996
12131
  }
11997
12132
  function lanLinuxUnitContent(opts) {
11998
12133
  // keep comments ASCII (avoid locale/encoding issues); ExecStart uses systemd's own quote rules
@@ -12022,7 +12157,7 @@ function lanCrontabLine(opts) {
12022
12157
  const parts = ['@reboot'];
12023
12158
  if (lanElectronHost(opts)) parts.push('ELECTRON_RUN_AS_NODE=1');
12024
12159
  parts.push(shQuote(process.execPath), shQuote(runtimeManagedScript()));
12025
- return parts.concat(lanServeArgs(opts)).join(' ') + ' >> ' + shQuote(lanLogPath(opts)) + ' 2>&1 ' + LAN_CRONTAB_MARKER;
12160
+ return parts.concat(lanServeArgs(opts).map(shQuote)).join(' ') + ' >> ' + shQuote(lanLogPath(opts)) + ' 2>&1 ' + LAN_CRONTAB_MARKER;
12026
12161
  }
12027
12162
  function lanCrontabRead() {
12028
12163
  try {
@@ -12101,6 +12236,12 @@ function lanLinuxRemoveCrontab() {
12101
12236
  return true;
12102
12237
  }
12103
12238
  function cmdLanLinuxEnable(opts) {
12239
+ const profileResult = lanWriteServeProfile(opts);
12240
+ if (profileResult.error) {
12241
+ console.error('服务 profile 写入失败,拒绝注册开机自启: ' + profileResult.text);
12242
+ process.exit(1);
12243
+ }
12244
+ opts = Object.assign({}, opts, { profilePath: profileResult.profilePath });
12104
12245
  if (lanLinuxHasSystemd()) {
12105
12246
  try {
12106
12247
  const unit = lanLinuxInstallSystemd(opts);
@@ -12143,6 +12284,23 @@ function cmdLanLinuxDisable() {
12143
12284
  }
12144
12285
  function cmdLanLinuxStatus() {
12145
12286
  let any = false;
12287
+ const profilePath = lanServeProfilePath();
12288
+ if (fs.existsSync(profilePath)) {
12289
+ console.log('服务 profile: ' + profilePath);
12290
+ const loaded = loadServeProfile(profilePath);
12291
+ if (loaded.error) {
12292
+ console.log(' 警告: ' + loaded.text);
12293
+ } else {
12294
+ console.log(' 记忆库: ' + loaded.profile.memoryHome);
12295
+ console.log(' 运行时: ' + loaded.profile.runtimeHome);
12296
+ const drift = serveProfileDrift(loaded.profile);
12297
+ if (drift.length) {
12298
+ console.log(' 警告: 服务 profile 与当前配置漂移(' + drift.join(', ') + '),请重新执行 yotta-memory lan enable');
12299
+ }
12300
+ }
12301
+ } else {
12302
+ console.log('服务 profile: 不存在 ' + profilePath + '(请重新执行 yotta-memory lan enable)');
12303
+ }
12146
12304
  const unit = lanLinuxUnitPath();
12147
12305
  const unitExists = fs.existsSync(unit) && lanFileHasMarker(unit);
12148
12306
  if (unitExists) {
@@ -12164,7 +12322,13 @@ function cmdLanLinuxStatus() {
12164
12322
  if (lanCrontabHasOurLine(lines)) {
12165
12323
  any = true;
12166
12324
  console.log('crontab @reboot: 已启用');
12167
- for (const l of lines) if (l.indexOf(LAN_CRONTAB_MARKER) !== -1) console.log(' ' + l.trim());
12325
+ for (const l of lines) {
12326
+ if (l.indexOf(LAN_CRONTAB_MARKER) === -1) continue;
12327
+ console.log(' ' + l.trim());
12328
+ if (!/--profile(?:\s|$)/.test(l)) {
12329
+ console.log(' 警告: crontab 命令未固定服务 profile;请重新执行 yotta-memory lan enable 修复');
12330
+ }
12331
+ }
12168
12332
  } else {
12169
12333
  console.log('crontab @reboot: 未启用');
12170
12334
  }
@@ -12177,9 +12341,11 @@ function cmdLanWinEnable(opts) {
12177
12341
  }
12178
12342
  const result = lanWindowsEnableCore(opts);
12179
12343
  if (result.mode === 'task') {
12344
+ opts = Object.assign({}, opts, { profilePath: result.profilePath });
12180
12345
  const triggerText = result.trigger === 'onstart' ? '开机触发(无需登录)' : '登录触发';
12181
12346
  console.log('已注册开机自启: 计划任务 ' + LAN_TASK_NAME + '(' + triggerText + ',后台静默运行,不弹窗口)');
12182
12347
  console.log('运行命令: ' + lanTaskRunCmd(opts));
12348
+ console.log('服务 profile: ' + result.profilePath);
12183
12349
  console.log('日志: ' + result.logPath);
12184
12350
  if (result.removedStartup && result.removedStartup.length) {
12185
12351
  console.log('已清理旧 Startup 兜底文件 ' + result.removedStartup.length + ' 个(避免双份自启)');
@@ -12188,6 +12354,10 @@ function cmdLanWinEnable(opts) {
12188
12354
  return;
12189
12355
  }
12190
12356
  if (result.error) {
12357
+ if (result.phase === 'profile') {
12358
+ console.error('服务 profile 写入失败,拒绝注册开机自启: ' + String(result.text || '').split(/\r?\n/)[0]);
12359
+ process.exit(1);
12360
+ }
12191
12361
  console.error('计划任务注册失败: ' + String(result.reason || '').split(/\r?\n/)[0]);
12192
12362
  console.error('写 Startup 自启失败: ' + String(result.text || '').split(/\r?\n/)[0]);
12193
12363
  console.error('请用管理员终端执行 yotta-memory lan enable,或将以下命令手动加入启动项: ' + lanTaskRunCmd(opts));
@@ -12200,6 +12370,7 @@ function cmdLanWinEnable(opts) {
12200
12370
  console.log('已启用用户级 Startup 静默自启(无需管理员)');
12201
12371
  console.log('启动脚本: ' + result.vbsPath);
12202
12372
  console.log('启动命令: ' + result.cmdPath);
12373
+ console.log('服务 profile: ' + result.profilePath);
12203
12374
  console.log('日志: ' + result.logPath);
12204
12375
  console.log('备注: 服务不会立刻启动,需重新登录后自动启动;如需现在运行请执行 yotta-memory serve');
12205
12376
  console.log('提示: 如需改回计划任务(后台静默),请用管理员终端重新执行 yotta-memory lan enable');
@@ -12236,9 +12407,12 @@ function cmdLanWinStatus() {
12236
12407
  return;
12237
12408
  }
12238
12409
  let taskFound = false;
12410
+ const profilePath = lanServeProfilePath();
12239
12411
  const q = lanWindowsRunSchtasks({}, ['/query', '/tn', LAN_TASK_NAME, '/fo', 'LIST', '/v']);
12240
12412
  if (q.status === 0) {
12241
12413
  taskFound = true;
12414
+ const xmlQ = lanWindowsRunSchtasks({}, ['/query', '/tn', LAN_TASK_NAME, '/xml']);
12415
+ const xmlSaysS4U = xmlQ.status === 0 && /<LogonType>\s*S4U\s*<\/LogonType>/i.test(String(xmlQ.stdout || ''));
12242
12416
  console.log('计划任务 ' + LAN_TASK_NAME + ': 已启用');
12243
12417
  const re = /^(状态|Status|登录模式|Logon Mode|上次运行时间|Last Run Time|上次结果|Last Result|要运行的任务|Task To Run)\s*[:=]\s*(.+)$/;
12244
12418
  let logonMode = '';
@@ -12250,12 +12424,31 @@ function cmdLanWinStatus() {
12250
12424
  if (/^(登录模式|Logon Mode)$/.test(m[1])) logonMode = m[2];
12251
12425
  if (/^(要运行的任务|Task To Run)$/.test(m[1])) taskRun = m[2];
12252
12426
  }
12253
- if (/interactive|交互式/i.test(logonMode)) {
12427
+ if (/interactive|交互式/i.test(logonMode) && !xmlSaysS4U) {
12254
12428
  console.log(' 提示: 登录模式为交互式(仅登录时运行,且在登录会话会弹窗口)——这是旧版任务;请重新执行 yotta-memory lan enable 修复为后台静默');
12255
12429
  }
12256
12430
  if (lanTaskCommandNeedsElectronEnv(taskRun)) {
12257
12431
  console.log(' 警告: 任务命令指向 Electron 可执行文件(如 YottaCode.exe)但缺少 ELECTRON_RUN_AS_NODE —— 开机将拉起桌面程序而非服务;请重新执行 yotta-memory lan enable 修复');
12258
12432
  }
12433
+ if (taskRun && !/--profile(?:\s|$)/.test(taskRun)) {
12434
+ console.log(' 警告: 任务命令未固定服务 profile,S4U / 无用户环境启动可能落到错误记忆库;请重新执行 yotta-memory lan enable 修复');
12435
+ }
12436
+ if (fs.existsSync(profilePath)) {
12437
+ console.log(' 服务 profile: ' + profilePath);
12438
+ const loaded = loadServeProfile(profilePath);
12439
+ if (loaded.error) {
12440
+ console.log(' 警告: ' + loaded.text);
12441
+ } else {
12442
+ console.log(' 记忆库: ' + loaded.profile.memoryHome);
12443
+ console.log(' 运行时: ' + loaded.profile.runtimeHome);
12444
+ const drift = serveProfileDrift(loaded.profile);
12445
+ if (drift.length) {
12446
+ console.log(' 警告: 服务 profile 与当前配置漂移(' + drift.join(', ') + '),请重新执行 yotta-memory lan enable');
12447
+ }
12448
+ }
12449
+ } else {
12450
+ console.log(' 警告: 服务 profile 不存在: ' + profilePath + ';请重新执行 yotta-memory lan enable');
12451
+ }
12259
12452
  } else {
12260
12453
  console.log('计划任务 ' + LAN_TASK_NAME + ': 未启用');
12261
12454
  }
@@ -13254,6 +13447,7 @@ async function main() {
13254
13447
  else if (a === '--id') opts.id = v;
13255
13448
  else if (a === '--time') opts.time = v;
13256
13449
  else if (a === '--tools') opts.toolProfile = v;
13450
+ else if (a === '--profile') opts.profile = v;
13257
13451
  else if (a === '--mcp-config') opts.mcpConfigPaths = (opts.mcpConfigPaths || []).concat(v);
13258
13452
  else if (a === '--skill-dir') opts.skillDirs = (opts.skillDirs || []).concat(v);
13259
13453
  else if (a === '--year') opts.years = (opts.years || []).concat(v);
@@ -13296,6 +13490,16 @@ async function main() {
13296
13490
  console.error('位置参数不足:yotta-memory remember <type> <subject> <statement>');
13297
13491
  process.exit(2);
13298
13492
  }
13493
+ if (first === 'serve' && opts.profile) {
13494
+ const applied = applyServeProfile(opts.profile);
13495
+ if (applied.error) {
13496
+ console.error('服务 profile 启动失败: ' + applied.text);
13497
+ process.exit(2);
13498
+ }
13499
+ opts.profile = applied.profilePath;
13500
+ if (opts.host === undefined && applied.profile.host) opts.host = applied.profile.host;
13501
+ if (opts.port === undefined && applied.profile.port) opts.port = applied.profile.port;
13502
+ }
13299
13503
  const runtimeIdentity = resolveIdentity(opts);
13300
13504
  setRuntimeAgent(runtimeIdentity.error ? '' : runtimeIdentity.id, runtimeIdentity.error ? '' : runtimeIdentity.agentKey);
13301
13505
  switch (first) {
@@ -13512,6 +13716,12 @@ module.exports = {
13512
13716
  runtimeDoctorCore: runtimeDoctorCore,
13513
13717
  runtimeEnsureForManagedTask: runtimeEnsureForManagedTask,
13514
13718
  runtimeRestartManagedServer: runtimeRestartManagedServer,
13719
+ lanServeProfilePath: lanServeProfilePath,
13720
+ resolveProfilePath: resolveProfilePath,
13721
+ lanWriteServeProfile: lanWriteServeProfile,
13722
+ loadServeProfile: loadServeProfile,
13723
+ applyServeProfile: applyServeProfile,
13724
+ serveProfileDrift: serveProfileDrift,
13515
13725
  lanTaskRunCmd: lanTaskRunCmd,
13516
13726
  lanStartupDir: lanStartupDir,
13517
13727
  lanAutostartDir: lanAutostartDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.24.0",
3
+ "version": "0.24.3",
4
4
  "description": "Yuanyi (元忆) — boundary-aware, file-based memory for AI agents. File-based, zero-dependency, diff/rollback-able; FACT/PREF/BOUND/COMMIT types (public shared / private isolated), user-level + project-level storage; v0.12 reliability baseline (init guard, trash-based deletion, independent-volume backup/list/doctor/restore, start-of-work doctor, transactional snapshots before destructive writes), v0.10 consolidation (periodic summaries with provenance, near-duplicate auto-merge, per-type decay, batch audit + rollback), v0.9 recall quality + context focus + optional local embedding plugin, plus v0.8 semantic search, feedback loop, self-organization and distillation.",
5
5
  "license": "MIT",
6
6
  "keywords": [
package/references/faq.md CHANGED
@@ -53,6 +53,9 @@ yotta-memory recall <关键词> --agent <id> --agent-key-file "<AI_HOME>/.yotta-
53
53
  ## 6.1 全局 CLI、MCP 或常驻服务版本不一致?
54
54
  先跑 `yotta-memory doctor --runtime`:它会列出 CLI、current、`runtime.json`、MCP 配置、运行中 server、技能副本与身份模式的漂移,并给出实际版本、期望版本、修复命令和是否阻断。用 `yotta-memory runtime install --from-current` 建立 `<runtimeRoot>/current` 稳定入口;升级用 `runtime install <tarball|版本>` + `runtime use <版本> --restart`,回滚用 `runtime rollback --restart`。`lan enable` 与备份调度只登记 `current/bin/yotta-memory.js`,不再写死版本目录。
55
55
 
56
+ ## 6.2 电脑异常重启后,局域网 MCP 一直返回 401?
57
+ 先运行 `yotta-memory lan status`,检查“服务 profile”的记忆库是否指向真实库,而不是 `C:\Users\Default\.yottamemory`。v0.24.1 起,`lan enable` 会生成每用户 `serve-profile.json`,任务命令应包含 `--profile <绝对路径>`。若仍显示旧任务未固定 profile,请在目标用户的管理员终端重新执行 `yotta-memory lan enable --onstart`,然后结束旧任务并从新任务启动。这样 S4U / 无登录会话启动不会重新猜测 home,也不会起在空库上。运行中的服务日志应显示 `服务 profile:` 与真实 `记忆库:`。
58
+
56
59
  ## 7. MCP 工具没加载?
57
60
  检查客户端 `mcpServers` 已配置 yotta-memory(url + token + agent_key);agent_key 应来自 `<AI_HOME>/.yotta-memory-agent-key` 或 MCP secret 注入,不把明文 key 写进对话。改配置后重启/重载会话。本机直连可不配 MCP,直接用 CLI。
58
61
 
@@ -1,4 +1,4 @@
1
- # yotta-memory 协议规范 v0.24.0
1
+ # yotta-memory 协议规范 v0.24.1
2
2
 
3
3
  > 本文件定义 yotta-memory 记忆标准:存储位置、目录结构、文件格式、类型体系与 CLI 命令参考。
4
4
  > 目标:任何支持 Agent Skills 开放标准的智能体,装完即可读写同一份记忆。
@@ -159,6 +159,8 @@ magic "YTMIDX1" (7B) | nonce(12B) | tag(16B) | ciphertext(JSON: {version, update
159
159
  | `reindex` | 全量扫描重建 `index.json`(手动改 .md 后校正)|
160
160
  | `index --stats [--json]` | 只读分析索引存储 / 逻辑体积、字段占比、token 与 gzip 年份分片预估 |
161
161
  | `layout plan/apply/rollback` | 生成日级目录迁移计划、按 journal 执行、失败回滚或按批次恢复 |
162
+ | `serve --profile <文件>` | 读取每用户服务 profile,固定 memoryHome / runtimeHome / configDir;缺失或无效时 fail-closed |
163
+ | `lan enable/disable/status` | 生成并检查每用户 `serve-profile.json`;自启任务只引用该 profile |
162
164
  | `export [--out f.json]` | 导出全部记忆为 JSON |
163
165
  | `import <f.json>` | 从 JSON 导入(幂等)|
164
166
  | `profile [--owner <id>]` | 用户画像聚合(零推断,写 `private/<owner>/profile.md`;跨 owner 默认拒绝)|
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-memory",
4
4
  "name": "元忆",
5
5
  "package": "@yottameta/yotta-memory",
6
- "version": "0.24.0",
6
+ "version": "0.24.3",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true