@istarry/dsh-assistant-profile 0.5.1 → 0.7.1

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/lib/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { Service } from "@deepseek-ai/cordis";
3
- import { defineTool } from "@deepseek-ai/dsh-tools";
4
- import "@deepseek-ai/dsh-llm";
5
- import "@deepseek-ai/dsh-session";
3
+ import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
6
  //#region ../tooling/node_modules/.pnpm/js-yaml@4.2.0/node_modules/js-yaml/dist/js-yaml.mjs
7
7
  /*! js-yaml 4.2.0 https://github.com/nodeca/js-yaml @license MIT */
8
8
  var __create = Object.create;
@@ -2511,9 +2511,148 @@ function compose(headerLines, entryLines) {
2511
2511
  return body.endsWith("\n") ? body : `${body}\n`;
2512
2512
  }
2513
2513
  //#endregion
2514
+ //#region ../.build/tsc/session-delete.js
2515
+ /**
2516
+ * 删除一个会话:磁盘日志、投影索引、工作区登记,最后通知前端把这一行拿掉。
2517
+ *
2518
+ * 这一版 harness 的会话存储(`sessionPersistence`)**没有** remove 方法,所以删除是
2519
+ * 自己做的:按磁盘布局(`<root>/<项目>/session-<id>/`)删掉会话目录,再尽力清理上层
2520
+ * 记账(索引行、工作区登记),最后发 `api-session/removed`——官方客户端的
2521
+ * session-controller 正是靠这个事件把行从侧栏移除(`remote.$on('api-session/removed')`)。
2522
+ *
2523
+ * 三条安全线:
2524
+ * 1. 正在跑的会话直接拒绝(`agent.status === 'running'`);
2525
+ * 2. 空闲但仍挂在注册表里的,先 detach(与 edit-regen 的做法一致);
2526
+ * 3. 只删“路径里带这个会话 id”的目录,拿不准就不删。
2527
+ *
2528
+ * @module @istarry/dsh-assistant-profile/session-delete
2529
+ */
2530
+ /**
2531
+ * 存储根目录:优先问持久化服务要,退一步用 `$DSH_HOME/sessions`。
2532
+ * @param persistence - 持久化服务。
2533
+ * @returns 绝对路径,或 undefined(两处都拿不到时放弃删除)。
2534
+ */
2535
+ function sessionsRootOf(persistence) {
2536
+ const root = persistence?.root;
2537
+ if (typeof root === "string" && root !== "" && existsSync(root)) return root;
2538
+ const home = process.env["DSH_HOME"];
2539
+ const fallback = home === void 0 || home === "" ? join(homedir(), ".dsh", "sessions") : join(home, "sessions");
2540
+ return existsSync(fallback) ? fallback : void 0;
2541
+ }
2542
+ /**
2543
+ * 找出一个会话在磁盘上的所有目录(分代文件可能让一个会话占多个目录)。
2544
+ * @param root - 存储根目录。
2545
+ * @param sessionId - 会话 id。
2546
+ * @returns 命中的目录列表(通常只有一个)。
2547
+ */
2548
+ function sessionDirsOf(root, sessionId) {
2549
+ const found = [];
2550
+ let projects = [];
2551
+ try {
2552
+ projects = readdirSync(root);
2553
+ } catch {
2554
+ return found;
2555
+ }
2556
+ const names = /* @__PURE__ */ new Set([sessionId, `session-${sessionId}`]);
2557
+ if (sessionId.startsWith("session-")) names.add(sessionId.slice(8));
2558
+ for (const project of projects) for (const name of names) {
2559
+ const dir = join(root, project, name);
2560
+ try {
2561
+ if (statSync(dir).isDirectory()) found.push(dir);
2562
+ } catch {}
2563
+ }
2564
+ return [...new Set(found)];
2565
+ }
2566
+ /**
2567
+ * 删除一个会话。
2568
+ * @param ctx - 宿主上下文。
2569
+ * @param sessionId - 要删的会话。
2570
+ * @returns 结果(失败时带原因码,由前端翻译)。
2571
+ */
2572
+ function deleteSession(ctx, sessionId) {
2573
+ const id = sessionId.trim();
2574
+ if (id === "") return {
2575
+ ok: false,
2576
+ outcome: "no session"
2577
+ };
2578
+ const agents = ctx.get("agents");
2579
+ const agent = agents?.get(id);
2580
+ if (agent?.status === "running") return {
2581
+ ok: false,
2582
+ outcome: "running"
2583
+ };
2584
+ if (agent !== void 0) {
2585
+ const entry = agents?.store?.get(id);
2586
+ if (entry === void 0 || typeof agents?.detachEntered !== "function") return {
2587
+ ok: false,
2588
+ outcome: "open"
2589
+ };
2590
+ try {
2591
+ agents.detachEntered(entry);
2592
+ } catch {
2593
+ return {
2594
+ ok: false,
2595
+ outcome: "open"
2596
+ };
2597
+ }
2598
+ }
2599
+ const root = sessionsRootOf(ctx.get("sessionPersistence"));
2600
+ if (root === void 0) return {
2601
+ ok: false,
2602
+ outcome: "delete failed"
2603
+ };
2604
+ const dirs = sessionDirsOf(root, id);
2605
+ if (dirs.length === 0) return {
2606
+ ok: false,
2607
+ outcome: "not found"
2608
+ };
2609
+ const removed = [];
2610
+ for (const dir of dirs) try {
2611
+ rmSync(dir, {
2612
+ recursive: true,
2613
+ force: true
2614
+ });
2615
+ removed.push(dir);
2616
+ } catch {
2617
+ return {
2618
+ ok: false,
2619
+ outcome: "delete failed"
2620
+ };
2621
+ }
2622
+ if (removed.length === 0) return {
2623
+ ok: false,
2624
+ outcome: "delete failed"
2625
+ };
2626
+ try {
2627
+ ctx.get("sessionQuery")?._deleteSession?.("persisted", id);
2628
+ } catch {}
2629
+ const registry = ctx.get("workspaceRegistry");
2630
+ try {
2631
+ registry?.forgetSession?.(id);
2632
+ } catch {}
2633
+ try {
2634
+ registry?.deleteKnown?.(id);
2635
+ } catch {}
2636
+ try {
2637
+ ctx.get("sessionProjectionCache")?.forget?.(id);
2638
+ } catch {}
2639
+ try {
2640
+ ctx.emit("api-session/removed", id);
2641
+ } catch {
2642
+ return {
2643
+ ok: true,
2644
+ outcome: "deleted (silent)"
2645
+ };
2646
+ }
2647
+ return {
2648
+ ok: true,
2649
+ outcome: "deleted"
2650
+ };
2651
+ }
2652
+ //#endregion
2514
2653
  //#region ../.build/tsc/index.js
2515
2654
  /**
2516
- * 助理档案 · 宿主半端。
2655
+ * Agent助理 · 宿主半端。
2517
2656
  *
2518
2657
  * 一个档案、一份人格、四个模式通用。人格的做法只有一件事:每个对话 agent 创建时,
2519
2658
  * 在这个 agent **自己的上下文**里注册一段 `deployment:persona-prefix`——作用域链上最近
@@ -2552,7 +2691,7 @@ const MEMORY_ORDER = 500;
2552
2691
  /** 人格插件的包名,兼容没有 `id` 的旧写法。 */
2553
2692
  const PERSONA_PLUGIN_NAME = "@deepseek-ai/dsh-persona";
2554
2693
  /**
2555
- * 运行时 schema。字段都是 volatile:它们由「助理档案」页与「Agent记忆」设置页
2694
+ * 运行时 schema。字段都是 volatile:它们由「Agent助理」页与「Agent记忆」设置页
2556
2695
  * 而不是插件配置页编辑,改动经设置通道写回 profile 的 patch,重启后仍在。默认值取
2557
2696
  * 内置「默认三」,所以装好就是一条带人设的鱼;三栏全部清空再保存,则什么都不注入。
2558
2697
  */
@@ -2563,7 +2702,9 @@ const Config = z.object({
2563
2702
  customsJson: z.string().default("[]").volatile(),
2564
2703
  memoryEnabled: z.boolean().default(true).description("Inject the assistant memory into the system prompt").volatile(),
2565
2704
  autoMemory: z.boolean().default(false).description("Let the model write memories on its own judgment (requires memoryEnabled)").volatile(),
2566
- memoryText: z.string().default("").description("The assistant memory document").volatile()
2705
+ memoryText: z.string().default("").description("The assistant memory document").volatile(),
2706
+ rememberSignal: z.string().default("").description("Browser-to-host mailbox for the remember button").volatile(),
2707
+ deleteSignal: z.string().default("").description("Browser-to-host mailbox for the session delete action").volatile()
2567
2708
  });
2568
2709
  /** 跨插件重载共享的当前配置引用:旧闭包也能读到新值。 */
2569
2710
  const LIVE = Symbol.for("dsh.assistant-profile.config");
@@ -2671,24 +2812,75 @@ async function installPersona(ctx, agent) {
2671
2812
  });
2672
2813
  }
2673
2814
  const MEMORY_ADD_DESCRIPTION = "Add one fact to the assistant's long-term memory when the conversation reveals something worth remembering across future sessions: stable preferences, work habits, durable decisions, or facts the human stated as lasting. Remember prose conclusions, not the raw exchange; merge related facts into one entry. Do not store credentials, one-off task details, or anything the human said is transient.";
2674
- /** `memory_add` 的输出结构。 */
2815
+ /**
2816
+ * `memory_add` 的输出结构。
2817
+ *
2818
+ * 这是**输出值**的 JSON Schema,不是工具参数的 `defineTool` 方言:`required` 必须是
2819
+ * 对象层上的字符串数组,写成属性上的 `required: true` 会被宿主的 JSON Schema 校验器
2820
+ * 拒绝(`unsupported JSON schema: ... required is not supported on type "boolean"`),
2821
+ * 而注册失败只会让工具消失——不报给用户。
2822
+ */
2675
2823
  const MEMORY_ADD_VALUE_SCHEMA = {
2676
2824
  type: "object",
2677
2825
  additionalProperties: false,
2678
2826
  properties: {
2679
- saved: {
2680
- type: "boolean",
2681
- required: true
2682
- },
2827
+ saved: { type: "boolean" },
2683
2828
  outcome: { type: "string" },
2684
2829
  content: { type: "string" }
2685
- }
2830
+ },
2831
+ required: ["saved"]
2686
2832
  };
2833
+ /** 把选中的对话文本提炼成记忆条目的指令(与 agent-suite 一致的规则)。 */
2834
+ const REMEMBER_INSTRUCTION = "你是一个长期记忆提炼引擎。用户从对话中圈选了下面这段内容,明确希望把它记入助手的长期记忆。请把这些内容提炼成简短、稳定、可跨会话复用的记忆条目。\n规则:\n- 每行输出一条记忆内容本身(不带时间戳、不带 \"- \" 前缀、不带编号)。\n- 用户既然圈选了它,请默认提取出其中明确、稳定的信息:称呼、偏好、习惯、约定、身份事实、项目背景。\n- 把相关事实合并成一条;不要逐字复述,只提炼结论,但保留关键名称、数字、关系。\n- 只有当整段内容完全是临时任务细节或含凭据、密钥时,才不输出条目(此时只输出一个真正的空行)。\n- 用与对话片段一致的语言书写(对话是中文就用中文,是英文就用英文)。\n- 严禁输出\"空行\"\"(空行)\"\"无\"\"没有\"\"none\"之类的占位文字。";
2835
+ /** 设置命名空间 = 本插件在 profile 里的条目 id。 */
2836
+ const SETTINGS_NS = "assistant-profile";
2837
+ /** 信箱里一次请求最多带多少字符(别把超长选区写进 profile 的 patch)。 */
2838
+ const REMEMBER_TEXT_LIMIT = 2e4;
2839
+ /** 信箱轮询间隔:0.7s 读一次字符串字段,人感觉不到延迟。 */
2840
+ const MAILBOX_POLL_MS = 700;
2841
+ /**
2842
+ * 一条用户消息。按线上形状手搭而不是引 `@deepseek-ai/dsh-llm`:宿主那边的包在插件
2843
+ * 目录里解析不到(只有 cordis 与 schemastery 两端共享),而消息只是 role/content/id
2844
+ * 三个字段的纯数据。
2845
+ * @param text - 消息正文。
2846
+ * @returns 可直接交给 llm.stream 的用户消息。
2847
+ */
2848
+ function userMessage(text) {
2849
+ return {
2850
+ id: crypto.randomUUID(),
2851
+ role: "user",
2852
+ content: [{
2853
+ type: "text",
2854
+ text
2855
+ }],
2856
+ source: { kind: "user" }
2857
+ };
2858
+ }
2687
2859
  /**
2688
- * 助理档案服务:人格注入 + 单份长期记忆。
2860
+ * 解析信箱字段;坏数据当空(宁可什么都不做,也不误跑一次)。
2861
+ * @param raw - 字段原文。
2862
+ * @returns 解析出的信号。
2863
+ */
2864
+ function parseSignal(raw) {
2865
+ if (raw.trim() === "") return void 0;
2866
+ try {
2867
+ const parsed = JSON.parse(raw);
2868
+ if (typeof parsed !== "object" || parsed === null) return void 0;
2869
+ return parsed;
2870
+ } catch {
2871
+ return;
2872
+ }
2873
+ }
2874
+ /**
2875
+ * Agent助理服务:人格注入 + 单份长期记忆。
2876
+ *
2877
+ * 记忆的存储、注入、两个开关、`memory_add` 工具与按钮提炼都在这里;客户端只负责
2878
+ * 设置页、聊天里的「记住」按钮和选择弹窗。
2689
2879
  *
2690
- * 记忆的存储、注入、两个开关、`memory_add` 工具和按钮提炼都在这里;
2691
- * 客户端只负责设置页与弹窗。
2880
+ * 按钮提炼没有自定义 RPC:浏览器把「要提炼哪段文本」写进 `rememberSignal`
2881
+ * (`phase: request`),宿主在 `loader/volatile-update` 里接手,用该会话自己的模型
2882
+ * 跑一次提炼,把结果写回同一个字段(`phase: done`)。写入统一走设置服务,所以记忆
2883
+ * 与它一样落进 profile 的 patch。
2692
2884
  */
2693
2885
  var AssistantProfile = class extends Service {
2694
2886
  config;
@@ -2700,8 +2892,16 @@ var AssistantProfile = class extends Service {
2700
2892
  static Config = Config;
2701
2893
  /** `memory_add` 工具的注册 disposer,只在开关允许时存在。 */
2702
2894
  toolDisposer;
2895
+ /** 正在处理一次按钮提炼:同一时间只跑一个。 */
2896
+ remembering = false;
2897
+ /** 上一次处理过的请求 id,防止同一请求被重复执行。 */
2898
+ lastRequestId;
2899
+ /** 删除信箱正在处理中。 */
2900
+ deleting = false;
2901
+ /** 上一次处理过的删除请求 id。 */
2902
+ lastDeleteId;
2703
2903
  constructor(ctx, config) {
2704
- super(ctx);
2904
+ super(ctx, "assistantProfile");
2705
2905
  this.config = config;
2706
2906
  live.config = config;
2707
2907
  ctx.inject(["settings"], (child) => {
@@ -2728,7 +2928,19 @@ var AssistantProfile = class extends Service {
2728
2928
  this.syncTool();
2729
2929
  ctx.on("loader/volatile-update", () => {
2730
2930
  this.syncTool();
2931
+ this.schedulePump();
2731
2932
  });
2933
+ ctx.effect(() => {
2934
+ const poller = setInterval(() => {
2935
+ this.syncTool();
2936
+ this.pumpRemember();
2937
+ this.pumpDelete();
2938
+ }, MAILBOX_POLL_MS);
2939
+ return () => {
2940
+ clearInterval(poller);
2941
+ };
2942
+ }, "assistant-profile: mailbox poll");
2943
+ this.schedulePump();
2732
2944
  }
2733
2945
  /** 按当前开关注册或撤下 `memory_add` 工具。 */
2734
2946
  syncTool() {
@@ -2742,43 +2954,53 @@ var AssistantProfile = class extends Service {
2742
2954
  /** 注册模型侧的记忆工具,返回撤下函数。 */
2743
2955
  registerTool() {
2744
2956
  const ctx = this.ctx;
2745
- const disposer = ctx.tools.register(defineTool({
2746
- name: "memory_add",
2747
- description: MEMORY_ADD_DESCRIPTION,
2748
- parameters: { text: {
2749
- type: "string",
2750
- required: true,
2751
- description: "The stable fact to remember, in its final prose form."
2752
- } },
2753
- output: {
2754
- schema: MEMORY_ADD_VALUE_SCHEMA,
2755
- render: (_args, value) => {
2756
- const v = value;
2757
- if (v.saved) return [{
2758
- type: "text",
2759
- text: `记住:${v.content ?? ""}`
2760
- }];
2761
- return [{
2762
- type: "text",
2763
- text: `未记住:${v.outcome ?? "未启用"}`
2764
- }];
2957
+ let disposer;
2958
+ try {
2959
+ disposer = ctx.tools.register({
2960
+ name: "memory_add",
2961
+ description: MEMORY_ADD_DESCRIPTION,
2962
+ parameters: {
2963
+ type: "object",
2964
+ additionalProperties: false,
2965
+ properties: { text: {
2966
+ type: "string",
2967
+ description: "The stable fact to remember, in its final prose form."
2968
+ } },
2969
+ required: ["text"]
2970
+ },
2971
+ output: {
2972
+ schema: MEMORY_ADD_VALUE_SCHEMA,
2973
+ render: (_args, value) => {
2974
+ const v = value;
2975
+ if (v.saved) return [{
2976
+ type: "text",
2977
+ text: `记住:${v.content ?? ""}`
2978
+ }];
2979
+ return [{
2980
+ type: "text",
2981
+ text: `未记住:${v.outcome ?? "未启用"}`
2982
+ }];
2983
+ }
2984
+ },
2985
+ execute: async (args) => {
2986
+ const trimmed = (typeof args.text === "string" ? args.text : "").trim();
2987
+ if (trimmed === "") return {
2988
+ saved: false,
2989
+ outcome: "empty fact"
2990
+ };
2991
+ const result = appendToText(this.config.memoryText.get(), trimmed);
2992
+ await this.writeMemory(result.text);
2993
+ return {
2994
+ saved: true,
2995
+ content: result.content,
2996
+ outcome: result.appended ? "appended" : result.refreshed ? "refreshed" : "unchanged"
2997
+ };
2765
2998
  }
2766
- },
2767
- execute: async (args, _exec) => {
2768
- const trimmed = args.text.trim();
2769
- if (trimmed === "") return {
2770
- saved: false,
2771
- outcome: "empty fact"
2772
- };
2773
- const result = appendToText(this.config.memoryText.get(), trimmed);
2774
- this.config.memoryText.set(result.text);
2775
- return {
2776
- saved: true,
2777
- content: result.content,
2778
- outcome: result.appended ? "appended" : result.refreshed ? "refreshed" : "unchanged"
2779
- };
2780
- }
2781
- }));
2999
+ });
3000
+ } catch (error) {
3001
+ ctx.logger.warn(`assistant-profile: memory_add registration failed: ${String(error)}`);
3002
+ return () => {};
3003
+ }
2782
3004
  const teardown = () => {
2783
3005
  disposer();
2784
3006
  this.toolDisposer = void 0;
@@ -2788,7 +3010,197 @@ var AssistantProfile = class extends Service {
2788
3010
  disposer();
2789
3011
  };
2790
3012
  }
3013
+ /**
3014
+ * 把一段记忆文本落进配置。只能经设置服务写:`Volatile` 只暴露 `get()`,
3015
+ * 宿主没有权限直接改自己的配置引用;写入经它之后,加载器会把新值就地提交进
3016
+ * 运行时引用(下一次组装提示词就读到),并落进 profile 的 patch。
3017
+ * @param next - 新的整段文本。
3018
+ */
3019
+ async writeMemory(next) {
3020
+ await this.persist({ memoryText: next });
3021
+ }
3022
+ /**
3023
+ * 经设置服务写回本插件的 volatile 字段。
3024
+ * @param patch - 要写入的字段。
3025
+ * @returns 是否写成功(失败只记日志,由调用方决定要不要重试)。
3026
+ */
3027
+ async persist(patch) {
3028
+ const settings = this.ctx.get("settings");
3029
+ if (settings === void 0) return false;
3030
+ try {
3031
+ await settings.update(SETTINGS_NS, patch);
3032
+ return true;
3033
+ } catch (error) {
3034
+ this.ctx.logger.warn(`assistant-profile: persist failed: ${String(error)}`);
3035
+ return false;
3036
+ }
3037
+ }
3038
+ /**
3039
+ * 把一次信箱处理排到当前事务之外。
3040
+ *
3041
+ * 设置写入本身跑在 HMR 事务里(`hmr.runExclusive`),而写回结果又要经设置服务再开
3042
+ * 一个事务——直接调用会撞上「HMR transactions cannot be nested」。所以触发点统一
3043
+ * 用 `setTimeout(0)` 推到事务之外,轮询那条路天然在事务外。
3044
+ */
3045
+ schedulePump() {
3046
+ setTimeout(() => {
3047
+ this.pumpRemember();
3048
+ }, 0);
3049
+ }
3050
+ /**
3051
+ * 检查信箱里有没有新请求,有就跑一次提炼并写回结果。
3052
+ *
3053
+ * 这里绝不向外抛:调用方是 `void`,一次未捕获的拒绝会被加载器当成插件加载失败。
3054
+ */
3055
+ async pumpRemember() {
3056
+ if (this.remembering) return;
3057
+ const signal = parseSignal(this.config.rememberSignal.get());
3058
+ if (signal?.phase !== "request" || signal.id === void 0) return;
3059
+ if (signal.id === this.lastRequestId) return;
3060
+ this.remembering = true;
3061
+ this.lastRequestId = signal.id;
3062
+ let result;
3063
+ try {
3064
+ result = await this.remember(signal.sessionId ?? "", signal.text ?? "");
3065
+ } catch (error) {
3066
+ result = {
3067
+ saved: false,
3068
+ outcome: `failed: ${errorChain(error)}`
3069
+ };
3070
+ }
3071
+ try {
3072
+ if (!await this.publishSignal({
3073
+ id: signal.id,
3074
+ phase: "done",
3075
+ saved: result.saved,
3076
+ entries: result.entries,
3077
+ outcome: result.outcome
3078
+ })) this.lastRequestId = void 0;
3079
+ } finally {
3080
+ this.remembering = false;
3081
+ }
3082
+ }
3083
+ /**
3084
+ * 把结果写回信箱(同一个字段),浏览器据此结束等待。
3085
+ * @param signal - 带结果的信箱内容。
3086
+ */
3087
+ async publishSignal(signal) {
3088
+ return await this.persist({ rememberSignal: JSON.stringify(signal) });
3089
+ }
3090
+ /** 检查删除信箱,有请求就删掉那个会话并把结果写回。 */
3091
+ async pumpDelete() {
3092
+ if (this.deleting) return;
3093
+ const signal = parseSignal(this.config.deleteSignal.get());
3094
+ if (signal?.phase !== "request" || signal.id === void 0) return;
3095
+ if (signal.id === this.lastDeleteId) return;
3096
+ this.deleting = true;
3097
+ this.lastDeleteId = signal.id;
3098
+ let outcome;
3099
+ try {
3100
+ outcome = deleteSession(this.ctx, signal.sessionId ?? "");
3101
+ } catch (error) {
3102
+ outcome = {
3103
+ ok: false,
3104
+ outcome: `failed: ${errorChain(error)}`
3105
+ };
3106
+ }
3107
+ try {
3108
+ if (!await this.persist({ deleteSignal: JSON.stringify({
3109
+ id: signal.id,
3110
+ phase: "done",
3111
+ ok: outcome.ok,
3112
+ outcome: outcome.outcome
3113
+ }) })) this.lastDeleteId = void 0;
3114
+ } finally {
3115
+ this.deleting = false;
3116
+ }
3117
+ }
3118
+ /**
3119
+ * 用该会话自己的模型把选中的对话文本提炼成记忆条目并追加。
3120
+ *
3121
+ * 会话必须还在线(agent 提供模型配置);提炼不写入对话历史,只发一次独立请求。
3122
+ * @param sessionId - 执行提炼的会话。
3123
+ * @param text - 选中的对话文本。
3124
+ * @returns 是否记住了、记住了什么、或失败原因。
3125
+ */
3126
+ async remember(sessionId, text) {
3127
+ const trimmed = text.trim().slice(0, REMEMBER_TEXT_LIMIT);
3128
+ if (trimmed === "") return {
3129
+ saved: false,
3130
+ outcome: "empty selection"
3131
+ };
3132
+ const agent = this.ctx.get("agents")?.get(sessionId);
3133
+ if (agent === void 0) return {
3134
+ saved: false,
3135
+ outcome: "session not open"
3136
+ };
3137
+ const callConfig = agent.session.requestHeader?.()?.config;
3138
+ if (callConfig === void 0) return {
3139
+ saved: false,
3140
+ outcome: "no provider/model available"
3141
+ };
3142
+ const llm = this.ctx.llm;
3143
+ let output = "";
3144
+ let failure;
3145
+ try {
3146
+ for await (const chunk of llm.stream({
3147
+ ...callConfig,
3148
+ messages: [userMessage(trimmed), userMessage(REMEMBER_INSTRUCTION)],
3149
+ sessionId: agent.session.id
3150
+ })) if (chunk.type === "text-delta" && typeof chunk.text === "string") output += chunk.text;
3151
+ else if (chunk.type === "finish") {
3152
+ const kind = chunk.reason?.kind;
3153
+ if (kind === "error" || kind === "aborted") failure = `analysis ${kind}: ${errorChain(chunk.reason?.failure)}`;
3154
+ else if (kind === "max-tokens") failure = "analysis truncated at the token cap";
3155
+ }
3156
+ } catch (error) {
3157
+ return {
3158
+ saved: false,
3159
+ outcome: `analysis failed: ${errorChain(error)}`
3160
+ };
3161
+ }
3162
+ if (failure !== void 0) return {
3163
+ saved: false,
3164
+ outcome: failure
3165
+ };
3166
+ const entries = splitLines(output);
3167
+ if (entries.length === 0) return {
3168
+ saved: false,
3169
+ outcome: "nothing worth remembering"
3170
+ };
3171
+ let next = this.config.memoryText.get();
3172
+ for (const entry of entries) next = appendToText(next, entry).text;
3173
+ await this.writeMemory(next);
3174
+ return {
3175
+ saved: true,
3176
+ entries,
3177
+ outcome: `remembered ${String(entries.length)}`
3178
+ };
3179
+ }
2791
3180
  };
3181
+ /** 把错误及其 cause 链压成一行。 */
3182
+ function errorChain(error) {
3183
+ const parts = [];
3184
+ let current = error;
3185
+ while (typeof current === "object" && current !== null && parts.length < 4) {
3186
+ const message = current.message;
3187
+ if (typeof message !== "string" || message === "") break;
3188
+ parts.push(message);
3189
+ current = current.cause;
3190
+ }
3191
+ return parts.join(" | ");
3192
+ }
3193
+ /** 把模型输出拆成一行一条记忆(去列表前缀、丢空行、丢占位词)。 */
3194
+ function splitLines(text) {
3195
+ const entries = [];
3196
+ for (const raw of text.split(/\r?\n/)) {
3197
+ const line = stripInvisible(raw.trim().replace(/^[-*]\s+/, "").trim());
3198
+ if (line === "") continue;
3199
+ if (isVacuousEntry(line)) continue;
3200
+ entries.push(line);
3201
+ }
3202
+ return entries;
3203
+ }
2792
3204
  /** 去掉模型可能在正文周围吐出的不可见字符。 */
2793
3205
  function stripInvisible(value) {
2794
3206
  return value.replace(/[\u0000-\u001F\u007F-\u009F\u00A0\u00AD\u1680\u2000-\u200B\u2028-\u202F\u205F-\u206F\u3000\uFEFF\uFFFD]/g, "").trim();
@@ -2801,4 +3213,4 @@ function isVacuousEntry(line) {
2801
3213
  return normalized === "" || normalized === "空行" || normalized === "无" || normalized === "没有" || normalized === "无内容" || normalized === "无值得记住的内容" || normalized === "暂无" || normalized === "none" || normalized === "no" || normalized === "nothing" || normalized === "na" || normalized === "n/a";
2802
3214
  }
2803
3215
  //#endregion
2804
- export { Config, MEMORY_SECTION, appendToText, dedupeKey, AssistantProfile as default, isVacuousEntry, name, parseEntries, renderPersona, stampNow, stripInvisible };
3216
+ export { Config, MEMORY_SECTION, appendToText, dedupeKey, AssistantProfile as default, deleteSession, isVacuousEntry, name, parseEntries, renderPersona, stampNow, stripInvisible };
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "@istarry/dsh-assistant-profile",
3
- "description": "助理档案:给 DSH 一份全局人格(名称 / 风格 / 语言 / 人格提示词),每个新会话的系统提示最前面都带着它;侧栏「新会话」上方显示助理名",
4
- "version": "0.5.1",
3
+ "description": "助理档案:给 DSH 一份全局人格(名称 / 语言 / 人格提示词)与一份长期记忆,每个新会话的系统提示最前面都带着它;聊天里点一下就能把选中的对话提炼进记忆;侧栏「新会话」上方显示助理名;会话行菜单里可以直接删除一个会话(日志、索引、工作区登记一起)",
4
+ "version": "0.7.1",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./cordis.patch.yml": "./cordis.patch.yml",
12
+ "./locale/*.json": "./locale/*.json",
13
+ "./package.json": "./package.json"
14
+ },
8
15
  "license": "MIT",
9
16
  "publishConfig": {
10
17
  "access": "public"
@@ -45,11 +52,6 @@
45
52
  "@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
46
53
  "@deepseek-ai/dsh-agent-preset-registry": "^0.2.1-rc.2",
47
54
  "@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
48
- "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2",
49
- "@deepseek-ai/dsh-typert-protocol": "*",
50
- "@deepseek-ai/dsh-tools": "*",
51
- "@deepseek-ai/dsh-llm": "*",
52
- "@deepseek-ai/dsh-session": "*",
53
- "@deepseek-ai/dsh-home-paths": "*"
55
+ "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2"
54
56
  }
55
57
  }