Skip to content
dsh.fish
Bundle

dsh-asset-library

DSH 素材库:会话内容一键保存 + 自动采集,按时间/文件类型/会话窗口三轴归纳,本地 Markdown 存储,侧边栏入口 + 管理页。

Source
AmigaMeow
License
MIT
Updated
Updated 4 days ago

Readme

# HARNESS

**DeepSeek 的 AI 产出物资产层。**

DeepSeek 负责生成内容,HARNESS 负责发现、整理、管理、复用这些 AI 产物。

它不是一个独立软件,而是像 Finder 之于 macOS —— 用户感觉它**一直在那里**,
而不是「打开了另一个系统」。

## 它解决什么

你在和 AI 工作,AI 帮你做出了东西(一份方案、一份分析、一个规格)。
这些产出散落在工作区里,几天后你只记得「好像做过」,找不到是哪个文件。

HARNESS 做三件事:

1. **发现** —— 扫工作区,认出哪些是「AI 帮你整理好的文件」(而不是依赖、脚手架、源码)
2. **确认** —— 侧栏提示「发现新资产」,你点保存或忽略
3. **沉淀** —— 存进本地 Markdown 资产库,按项目/时间/类型/会话归类

## 产品形态

停靠在**右侧 25%**,对话区保留 75%。不做弹窗、不做 Dashboard。

```
┌───────────────────────────┬─────────────┐
│                           │  HARNESS    │
│                           │             │
│      DeepSeek 对话         │  发现区      │
│         (75%)             │  我的资产    │
│                           │   (25%)     │
└───────────────────────────┴─────────────┘
```

视觉方向:Finder / VS Code Explorer / Notion Side Peek。
安静的文档列表,不是管理后台 —— 无大卡片、无统计看板、无图标堆砌。
配色跟随 DSH 主题,浅色为主。

## 核心:资产发现评分

工作区里 90 个文件,真正值得沉淀的可能是 6 个。**全收等于没收。**

所以每个文件会打一个 0-100 的「AI 资产分」:

| 维度 | 分值 |
|---|---|
| Markdown 结构(标题层级、正文段落) | +20 |
| 标题完整 | +10 |
| 目录存在(≥3 个小标题) | +10 |
| AI 语言特征(「以下是」「综上」「方案」…) | +20 |
| 来自工作区顶层 | +30 |

**新鲜度单独计,不进推荐分。** 早期版本把「生成后已沉淀」当加分项,
结果和产品场景直接冲突:核心场景是「AI 刚帮我做完,我顺手沉淀」,
而那正是分数最低的时刻(刚生成 5 秒 = 55 分被 60 的阈值挡掉,要等 6 分钟才推荐)。
判断一份文件是不是 AI 产出,跟它是不是刚写的无关 —— 所以新鲜度只用于**排序**:

| 生成后 | 新鲜度 |
|---|---|
| ≤10 分钟 | +15(最该被看到) |
| ≤1 小时 | +8 |
| ≤1 天 | +3 |

**硬性排除**:`node_modules`/`.git`/`dist`、脚手架文件(`package.json`/`README.md`/
`LICENSE`)、凭据、图片/压缩包/源码 —— 这些直接判 0。

实测:`/root/Deepseek` 下 90 个候选 → 推荐 6 个,全是真正的 AI 产出文档。

## 自动出现

产品场景是「AI 刚帮我做完,我顺手沉淀」,所以不该等用户想起来点侧栏。

- 每 15 秒问一次发现区,**有新产出就把侧栏展开**
- 只展开侧栏,**不弹窗、不抢焦点** —— 用户正在写作,打断比不提示更糟
- 用户手动收起后安静 5 分钟,不再自动弹出(尊重用户意图)
- 数量没变就不重复展开

## 资产状态

每份资产有一个状态:**草稿** → **已确认** → **已归档**。

- 自动采集/扫描进来的 = `draft`(草稿)
- 用户主动点「保存」的 = `confirmed`(已确认)
- 详情里可随时切换,侧栏只给非草稿显示标签(草稿是默认态,挂标签没信息量)

## 命名:内容优先

**「AI 负责命名,用户只负责确认」** —— 标题优先取正文里的 H1,不是磁盘文件名:

| 情况 | 标题 |
|---|---|
| 文档有 H1 | `小米摄像头 fnOS 插件 — 项目设计文档`(H1) |
| 显式指定 | 用户/模型给的那个 |
| 代码/数据文件 | 文件名(首行是 `const a = 1`,当标题很怪) |
| 文档无 H1 | 正文首个实文行 |
| 全空 | 文件名 |

`deriveTitle` 会跳过 YAML front matter,避免把 `---` 或 `title: xxx` 当标题。

## 会话感知

侧栏会**跟随当前会话**:切换会话时,「当前会话」区列出的是那个会话沉淀的资产。

实现要点:DSH 的 `ctx.uiSession` 暴露 `currentBinding.sessionId`,外置插件要拿到它
必须在 `package.json` 的 `dsh.client.inject` 里声明
`@deepseek-ai/dsh-client-ui-session`(官方插件也是这么做的)。
DSH 没给客户端插件会话切换事件,所以客户端低频对比一次,变了就通知面板重拉。

拿不到会话 id 时**退回「全部资产」视图**,不报错。

## Markdown 阅读器

详情里直接渲染标题 / 列表 / 代码块 / 表格 / 引用 / 链接,不再是 `<pre>` 原文。

**长文档有目录**:标题 ≥4 个时自动生成可折叠目录,点击跳转。

```
目录 · 41 节
  MiCam 开发笔记(cs2 直连踩坑全记录)
    一、go2rtc 使用要点
      1.1 Dial 内部已完成握手
      1.2 Producer 是推模式,必须有人读
```

实测 546 行的真实文档生成 41 条层级目录。细节:

- 标题生成唯一锚点 id(中文保留,重名自动加序号)
- 影子树里 `#anchor` 跳转不生效(浏览器只查文档级),所以点击是**手动 scrollIntoView**
- 短文档(<4 个标题)不给目录,免得啰嗦

自己写(`lib/markdown.js`),不引外部依赖 —— 引 marked / markdown-it 会给插件
加一棵依赖树和一个构建步骤,违背「dependencies 为空」的铁律。

**安全**:文档内容是不可信输入。渲染前**全部转义**,链接只放行 http/https
(`javascript:` / `data:` / `vbscript:` 一律降级为纯文本)。
13 个 XSS 向量在测试里覆盖(script / img onerror / svg onload / iframe /
属性注入 / 大小写绕过 / 表格与引用内注入…)。

## 视觉方向

**不要**:Dashboard、大卡片、数据统计、图标堆砌、深色科技风、抽屉弹出动画。

**要**:原生集成、极简侧栏、文档列表、AI 发现提示、小而精致、像系统功能。

右侧面板 300ms 平滑出现(macOS Finder 侧栏的手感),
能停靠就停靠进 AppFrame 的右栏,停靠不了才退化为固定定位。

## 📦 安装

```sh
dsh plugin --profile web add /path/to/dsh-asset-library
# 重启 dsh web 生效
```

要求 `pnpm` 在 PATH 上(没有就先 `corepack prepare pnpm@latest --activate`)。

装完侧边栏会出现「素材库」入口,点开是管理页;也可以直接访问
`<dsh web 地址>/ext/asset-library/`。

## 🗂 数据长什么样

```
<DSH_HOME>/asset-library/
├── time/2026-09/
│   └── 2026-09-14-接口设计稿-3f9a2b.md
├── type/markdown/
│   └── 2026-09-13-读后笔记-a1b2c3.md
└── session/素材库开发-c7b1b0f9/
    └── 2026-09-13-会话结论-9d8e7f.md
```

每个 `.md`:

```markdown
---
id: "ast-c500f747-..."
title: "项目轴验证"
kind: "file"
axis: "time"
created: "2026-09-14T01:26:25.013Z"
session_id: "session-5549d9ae-..."
session_title: "素材库开发"
source_path: "/root/Deepseek/dsh-asset-library/lib/store.js"
file_type: "javascript"     # 细粒度类型名
category: "code"            # 语义类别(类型轴的桶)
project: "素材库插件"        # 项目维度
tags:
  - "代码"
  - "dsh-asset-library"
summary: "项目归类测试"
---

项目归类测试
```

**手写友好**:直接把一个自己写的 `.md` 丢进库目录,下次载入就会被索引;
缺 front matter、字段残缺都不会报错。

## 📥 导入历史会话

面板顶栏的**「导入历史会话」**按钮,把 DSH 已有的会话日志提炼成素材。

不是原样倾倒 —— 你机器上 6 个会话有 **8000+ 个事件**,全量入库只会是一堆噪音。
提炼规则(`lib/session-import.js`):

| 来源 | 产出 |
|---|---|
| 每个会话 | 一条**会话纪要**(标题、轮次、提问数、产出数、前几条提问) |
| 会话产出的文件(`write`/`edit` 等) | 逐条登记为 `file` 素材,带真实路径 |
| 助手回复中 **> 200 字**的结论 | 单独存为 `note` |

实测:**8125 个事件 → 91 条素材**。

几个刻意的取舍:
- **只认人真正敲的消息**(`source.kind === 'user'`)当提问与标题 —— 会话日志里混了大量插件注入内容(工具结果回灌、审批策略通知、技能目录),拿来当标题会很难看
- **`reasoning` 块不入库** —— 那是模型的思考过程,不是结论
- **重复导入会跳过**已导入的会话(也可强制重扫)
- 推不出项目名时回落到会话标题,而不是编一个名字

> 会话日志是**多帧 zstd**(每次写入追加一帧,单文件可达上千帧),
> `zstdDecompressSync` 只解第一帧。必须按 magic number 切帧逐帧解压。

## ⌨️ 斜杠命令

在会话输入框里直接敲,**不进入模型请求**,立即执行:

| 命令 | 作用 |
|---|---|
| `/save` | 保存上一条助手回复(标题自动从正文首行推导) |
| `/save <标题>` | 同上,但用你给的标题 |
| `/save note <正文>` | 把 `<正文>` 直接存为笔记 |
| `/save list [关键词]` | 列出素材,可按关键词过滤 |
| `/save help` | 用法 |

## 🤖 模型工具

| 工具 | 用途 |
|---|---|
| `asset_save_file` | 保存一个文件(文本会内联正文;二进制/超大只登记路径)。同路径重复保存自动去重 |
| `asset_save_snippet` | 直接保存一段文本/结论/代码,不依赖磁盘文件 |
| `asset_list` | 列表 + 过滤(`axis` + `bucket`,或 `project`,或关键词 `q`,或 `kind`) |
| `asset_overview` | 总览:三轴各有哪些桶、各多少条 |
| `asset_read` | 读一条素材的全文 |

## 🔌 HTTP API

| 方法与路径 | 说明 |
|---|---|
| `GET /ext/assets/views` | 三轴视图(桶 + 计数) |
| `GET /ext/assets/list?axis=&bucket=&q=&kind=&limit=&offset=` | 列表 |
| `POST /ext/assets/register` | 登记一条素材 |
| `GET /ext/assets/<id>` | 元数据 |
| `GET /ext/assets/<id>/content` | 元数据 + 全文 |
| `GET /ext/assets/<id>/file` | 下载那个 `.md` |
| `PATCH /ext/assets/<id>` | 改标题/摘要/标签/正文 |
| `DELETE /ext/assets/<id>` | 删除条目与文件 |
| `GET /ext/asset-library/` | 管理页 |

**安全边界**:`webServer` 本身没有鉴权中间件,而 profile 可能绑到 `0.0.0.0`
供局域网访问。因此**所有写操作、以及与文件正文相关的读取,仅限回环来源**
(判据是 TCP 层的 `socket.remoteAddress`,不可伪造)。非本机来源只拿到不含正文的只读元数据。

## ⚙️ 配置

在 profile 的 `cordis.patch.yml` 里覆盖(全部可选):

```yaml
- id: asset-library
  config:
    rootDir: /your/library        # 默认 <DSH_HOME>/asset-library
    autoCollect: true             # 默认 true
    maxPerTurn: 20                # 单轮自动采集上限
    mutationTools: [write, edit]  # 视为「文件变更」的工具名
    maxInlineBytes: 262144        # 超过就不内联正文,只登记路径
```

## 🧱 为什么 `dependencies` 是空的

这不是洁癖,是一个踩过的坑。

DSH 的模块解析会**优先命中 `~/.dsh/profiles/<name>/node_modules`**,遮蔽
`/opt/dsh/.../node_modules` 里的宿主版本。如果一个插件在自己的 `dependencies`
里写了宽松范围的 `@deepseek-ai/*`,pnpm 很可能装进一个与宿主版本不匹配的副本,
然后整棵 agent 树就启动失败。实测中 `dsh-artifact-library` 就是这样让
`dsh web` 起不来的:

```
SyntaxError: The requested module '@deepseek-ai/dsh-llm'
  does not provide an export named 'CallId'
```

所以本包:

- `dependencies` / `peerDependencies` 全空;
- 不 import 任何 `@deepseek-ai/*`;
- `DSH_HOME` 自己解析(`lib/paths.js`),工具定义手写(不用 `defineTool`);
- 宿主能力全部通过 `ctx` 服务在运行时取用(`tools` / `webServer` / `systemPrompt`),
  用 `ctx.inject` 惰性挂载 —— headless profile 里没有 `webServer` 也不会挂。

装完可以自查:`ls ~/.dsh/profiles/web/node_modules/@deepseek-ai` 应该是空的。

## 🧪 测试

```sh
npm run smoke              # 九套一起跑
npm run smoke:markdown     # 34 项:Markdown 渲染 + 13 个 XSS 向量
npm run smoke:content      # 37 项:语义分类、标题/摘要/标签推导、项目推断
npm run smoke:artifact     # 22 项:资产评分、新鲜度分离、排序
npm run smoke:import       # 24 项:多帧 zstd 解压、会话提炼、重复导入跳过
npm run smoke:host         # 51 项:front matter 往返、四轴分桶、原子写、查重、搜索、分页…
npm run smoke:tools        # 用 DSH 真实 ToolRuntime 校验 5 个工具的 schema
npm run smoke:commands     # 30 项:/save 各分支、助手回复提取的防御式兼容
npm run smoke:http         # 19 项:每个路由的状态码/内容类型/安全边界
npm run smoke:ui           # 35 项:jsdom 真实文档 —— 可见性、几何、会话切换(需 jsdom)
```

`smoke-tools` 需要能找到 DSH 安装目录(默认 `/opt/dsh/lib/node_modules/@deepseek-ai/dsh`),
用 `DSH_ROOT` 覆盖。

## ⚠️ 已知限制

- **侧边栏入口是 DOM 注入的**。DSH 目前没给外部插件开放 `sidebar.panellist` 槽位
  (共享模块表里没有 `dsh-client-ui-sidebar`),社区插件(link-collect、
  skill-explorer、dshmarket)走的都是同一条路:注入一行 DOM + MutationObserver 自愈。
  管理页因此是独立整页,不是原生的 `main` 面板。等官方开放后可平滑替换这一层。
- **没有文件系统监听**。库目录被外部改动后,需要点管理页的「刷新」(或重启)才会重新索引。
- **正文内联有上限**,二进制文件只登记路径与元数据,不在页面里预览。

## 📄 License

MIT

Install

dsh plugin --profile web add github:AmigaMeow/dsh-asset-library

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source