aidsh
lswn/aidsh
DeepSeek Harness plugin: normalise author and creation date on generated docx/xlsx/pptx/pdf and friends.
Install
dsh plugin --profile web add github:lswn/aidshREADME
dsh-office-meta-stamp
DeepSeek Harness 插件:给自动生成的文档按你的名字和生成日期署名。
生成 docx / xlsx / pptx / pdf 时,工具链往往把自己的名字写进文件属性(python-docx、openpyxl、Steve Canny,甚至 2013 年的模板日期)。这个插件在文件落盘后,只改“作者”和“创建/修改时间”,其余字节一个不动。作者名已经是你自己的文件,完全不动。
本 README 分两部分:第一部分给人读;第二部分是给 AI / 维护者的复现规格。 零运行时依赖,只用 Node 内置模块。
第一部分 · 给人看
它解决什么问题
| 你看到的 | 实际来源 |
|---|---|
属性里作者是 python-docx | docProps/core.xml 的 dc:creator |
| 创建时间是 2013 年 | 库自带的模板日期 |
备注里写着 generated by python-docx | dc:description |
最后修改者是 Steve Canny | cp:lastModifiedBy |
插件把这些换成你配置的作者和文件自身的生成时间,并删掉自定义属性里的生成器痕迹。
安装
# 1) 按包名(发布到 npm 之后)
dsh plugin --profile desktop add dsh-office-meta-stamp
# 2) 本地目录(开发时用)
dsh plugin --profile desktop add link:"D:\Plugins\dsh-office-meta-stamp"
# 3) Web 侧栏「插件 → 添加插件」里贴包名 / Git 地址 / 本地目录
link: 只建目录联接,不复制文件。插件零依赖,所以本地链接安装不需要额外解析任何包。
装完重启一次 DSH:Host 侧插件只在启动时加载。
装完是什么样
- 插件加载时如果
author为空,自动用当前系统用户名。 - 之后每次生成文档,落盘后属性自动变成:作者 = 你,创建/修改 = 生成那一刻。
- 想换名字,改 profile 里的一行配置再重启。
没有弹窗,没有打断。
配置
当前 DSH 版本的插件页只为官方配置插件渲染表单,第三方插件没有设置界面。 配置直接写在 profile 的
cordis.patch.yml:
- id: office-meta-stamp
name: dsh-office-meta-stamp
config:
author: "你的名字"
debug: false
文件位置:%USERPROFILE%\.dsh\profiles\<profile>\cordis.patch.yml。改完重启一次。
| 字段 | 默认 | 说明 |
|---|---|---|
author | 系统用户名 | 写进文件的作者名 |
lastModifiedBy | 空 | 留空表示与 author 相同 |
notifySkipped | true | 无法改写时是否写 WARN 日志 |
allowMacro | false | 放行带宏的 OOXML 包(会破坏签名) |
maxBytes | 50 MB | 超过直接跳过 |
dryRun | false | 只记录,不落盘 |
pdfMode | metadata-only | off 表示完全不处理 PDF |
debug | false | 把每个决策写进系统临时目录的 office-meta-stamp-debug.log |
支持、拒绝、跳过
支持(会改)
.docx .xlsx .pptx .xlsb → docProps/core.xml
.odt .ods .odp → meta.xml
.epub → OPF 的 dc:creator / dcterms:modified
.rtf → {\info} 组
.pdf → 追加新的 /Info(增量更新)
拒绝(生成前就拦下,不会产出属性错误的文件)
.doc .dot .xls .xlt .ppt .pot 旧版 OLE 复合文档,就地改扇区不可逆
.docm .xlsm .pptm 改动会破坏数字签名,默认拒绝
.wps .et .dps WPS 私有/旧格式,结构不公开
.pages .numbers .key iWork,元数据在 IWA 里,无安全写路径
同一会话里第一次拦下会提示改用 .docx;再试只回“该格式不支持”,不再给方案。
跳过(文件保留原样,只记日志)
加密 / ODF 带数字签名 / 文件损坏 / 超过 maxBytes / 被占用 / 结构无法识别。
什么时候会处理
插件看不到意图,只能读工具调用的参数:
- 判早了:写
gen.py时脚本内容里有报告.docx,会在写脚本那步就处理。无实际影响。 - 判漏了:文件名由变量拼出来,参数里没有扩展名,抓不到。兜底是工具执行后对相关目录做限时扫描(1.5 秒防抖、只看 20 秒内变动的文件、深度 2 层)。
两条边界,避免误伤:
- 只读工具不触发:
read/glob/grep/present不会引起落章。 - 只处理刚生成的文件:参数命中的文件,修改时间超过 10 分钟的跳过。
安全承诺
- 作者名已经是你的文件完全不动——日期不动、字节不动。你自己在 Word 里做的文档不会被“改成”插件认为对的样子。
- 只替换 zip 里的目标条目,其余字节原样搬运;PDF 只追加,不动原字节。
- 先写临时文件、校验通过再原子替换;失败保留原文件。
- 不动文件系统时间。
- 不动
app.xml的Application、ODF 的meta:generator、PDF 的Producer。 - 写
README这类正文里提到旧格式的文件,不会再被门禁误拦。
已知限制
- 加密文件(OOXML 加密码后其实是 OLE 容器)一律跳过。
- PDF 的
/Info位于对象流中时跳过,不重建(避免丢字段)。 - 混合交叉引用(
/XRefStm)的 PDF 跳过。 - 不回溯修改已存在的文件;不支持多作者 / 按项目切换。
- xlsx 的
Application仍是openpyxl。想彻底干净,生成后在 Excel/WPS 里打开另存一次。
常见问题
装完没反应? 重启一次 DSH;确认装到了你在用的 profile。
作者还是旧名字? 看 cordis.patch.yml 里 author,或者重启让系统用户名回退生效。
某个文件没改? 打开 debug: true,看临时目录的日志;notifySkipped 也会写 WARN。
第二部分 · 给 AI 读(复现规格)
目标:读完这一节,能在没有本仓库的情况下重新实现同等行为,或安全地修改它。
0. 复现清单
node test/run.mjs # 期望:17 passed, 0 failed
node test/adversarial.mjs # 期望:34 passed, 0 failed
零运行时依赖,只用 node:zlib、node:fs、node:path、node:os。Node ≥ 20。
1. 运行契约
export const name = 'office-meta-stamp'
export const inject = ['tools']
export function apply(ctx, config) { /* 注册两个事件处理器 */ }
没有 Config 导出:宿主不渲染第三方插件的表单,配置由 loader 从 YAML 直接传入。
package.json 必须含:
"type": "module",
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
cordis.patch.yml 向 profile 插入条目,id 必须与插件用于持久化的键一致:
- insert:
- id: office-meta-stamp
name: dsh-office-meta-stamp
两个事件(返回值即决策):
ctx.on('tools/pre-execute', async (exec, next) => ({ kind: 'allow' } | { kind: 'deny', reason }))
ctx.on('tools/post-execute', async (exec, result, next) => decision)
exec.name/exec.arguments(已冻结,不可改写)。pre-execute返回对象即决策,不调用next()。post-execute必须先await next();处理全包在 try/catch 里,绝不能让插件异常导致工具调用失败。
2. 不变量
- 不破坏:先在内存构造新 buffer、自检、再原子替换;失败保留原文件。
- 字节最小改动:zip 只换目标条目;PDF 只追加。禁止“解包 → 重组”。
- 不猜:结构不认识就
SkipError,绝不尝试修复。 - 幂等:作者名已等于目标值 →
changed:false,字节与日期都不动。 - 属性保留:替换元素时保留原属性(命名空间可能声明在元素上,见 §10)。
- 不改文件系统时间:
fs.utimes还原 atime/mtime。 - 时间来自文件:
created = birthtime ?? mtime,modified = mtime;不用Date.now()。 - 只碰刚生成的文件:只读工具不触发;参数命中的文件 mtime 早于 10 分钟前则跳过。
3. 模块地图
| 文件 | 职责 |
|---|---|
lib/index.js | 入口:配置归一化、两个事件、防抖扫描、日志 |
lib/config.js | DEFAULTS 与 unwrap();没有 schema |
lib/classify.js | 从参数结构里找扩展名与路径;按工具决定扫描范围 |
lib/normalize.js | 嗅探 → 适配器 → 幂等 → 校验 → 原子写 |
lib/zip.js | 外科式 ZIP 读写(EOCD/CD/LFH/CRC32) |
lib/xml.js | 保留属性的元素文本替换 |
lib/time.js | W3CDTF / ODF / PDF / RTF 四种时间格式 |
lib/debug.js | 可选的 JSONL 诊断日志 |
lib/adapters/*.js | 每种格式的 stamp(buffer, meta) |
适配器契约:
export const id = 'opc'
export const extensions = ['.docx']
export function stamp(buffer, meta) // → { buffer, format, mode? } | throw SkipError
// meta = { author, lastModifiedBy, created: Date, modified: Date }
4. 数据流
工具调用
├─ pre-execute → scopedArguments(name, args) → classify → rejected[] → deny(两段式)
└─ 执行 → post-execute
└─ afterTool(只读工具直接返回)
├─ collectWorkingDirs(scoped)
├─ pathsIn(scoped, resolve)(只取 mtime ≤10min 的)
├─ stampOne(file)
└─ scheduleSweep() // 1.5s 防抖,depth=2,20s 窗口,最多 400 个文件
stampOne → stampFile → normalizeBuffer:
stat(size, mtime, birthtime) → 超限跳过 → author 为空跳过
→ readFile(EBUSY 退避重试)
→ sniff: PK..(zip) / D0CF11E0(ole) / %PDF / {\rtf
→ 容器与适配器一致性校验
→ 幂等检查(仅作者名)
→ adapter.stamp → zip 结果再 readZip 验证
→ 写 .<name>.<pid>.<ts>.stamp.tmp → rename(EBUSY/EPERM/EACCES 退避 5 次)
→ utimes 还原
5. 算法
5.1 ZIP 外科改写
读:从尾部向前 ≤65557 字节找 EOCD(0x06054b50);拒绝 ZIP64 与多卷;遍历中央目录(0x02014b50),校验每条本地头(0x04034b50)。
写:按 localOffset 排序;未改条目按原始字节区间整段复制(区间终点 = 下一条目的 localOffset,最后一条用 cdOffset),额外字段、数据描述符、压缩参数全部保留。改动条目重建本地头(flags & ~0x0008,sizes 写入本地头),deflateRawSync,压缩后不小于原文则改 store。最后重建中央目录与 EOCD,保留归档注释。
5.2 OPC(docx/xlsx/pptx/xlsb)
- 有加密条目 → Skip。
docProps/core.xml必须存在。setElementText逐字段:dc:creator、cp:lastModifiedBy、dcterms:created、dcterms:modified、cp:revision=1;删dc:description。- 有
docProps/custom.xml→ 删条目,并从_rels/.rels与[Content_Types].xml清掉引用。
5.3 ODF / EPUB
- ODF:有
documentsignatures.xml或 manifest 含encryption-data→ Skip。改meta.xml的meta:initial-creator、dc:creator、meta:creation-date、dc:date、meta:editing-cycles。 - EPUB:
META-INF/container.xml→<rootfile full-path>→ OPF;改dc:creator、dc:date、<meta property="dcterms:modified">(EPUB3 强制,必须同步)。
5.4 RTF
定位 {\info;括号扫描(跳过 \x 转义)找组尾;组内替换 \author / \creatim / \revtim 的控制字参数,缺失则插入。无 info 组时在 {\rtf1 头之后插入。非 ASCII 用 \uN?(有符号 16 位),增补平面拆成代理对。
5.5 PDF 增量更新
- 取最后一个
startxref→ 偏移;该处是xref表或N 0 obj交叉引用流。 - 从 trailer / 流字典取
/Root、/Info、/ID;/Encrypt或/XRefStm→ Skip。 - 新 Info 号 =
maxObjNum + 1;原/Info存在时必须找到其明文<< >>并继承字段(Title/Producer 等);找不到(在对象流里)→ Skip。 - 追加:
Info对象 → 新 xref(表:xref\nN 1\n<off> 00000 n;流:/Type /XRef /W [1 4 2] /Index [N 2],deflate 7 字节/条)→ trailer 带/Prev→startxref→%%EOF。 - 自检:重解析
startxref、确认/Info N 0 R可达、原字节是结果前缀。
字符串:ASCII 用 (...),非 ASCII 用 UTF-16BE 十六进制 <FEFF...>。日期 D:YYYYMMDDHHmmSS±HH'mm'。
6. 判定与门禁
- 扩展名按长度倒序匹配(
.docx不会变成.doc),后缀断言(?![a-z0-9])。 - 扫描范围按工具区分:
write/edit等目标明确的工具只看path类字段;shell 类工具才扫整条命令。否则写一份提到旧格式的 README 会被自己拦下。 - 门禁只拦
REJECTED_EXTENSIONS;allowMacro打开时带宏包走 OPC 适配器。 - 两段式文案按
(sessionId, extension)计数:首次给替代格式,其后只回“不支持”。
7. 跳过原因
| 原因 | 触发 |
|---|---|
| 文件已加密 | zip 条目加密位 / PDF /Encrypt / ODF manifest |
| 数字签名 | ODF documentsignatures.xml |
| 旧版二进制 | 魔数 D0 CF 11 E0 |
| 超过体积上限 | > maxBytes |
| 被占用 | 读取或 rename 重试耗尽 |
| 结构无法识别 | 缺 core.xml / meta.xml / OPF / trailer |
8. 测试规格
test/run.mjs(17 项,主路径):
- zip 只改一个条目,其余字节不变
- OLE 输入被拒绝
- docx:作者/时间写入、
dc:description消失、dc:title保留 - docx 幂等
- odt:
meta.xml更新、meta:generator保留 - epub:
dc:creator+dcterms:modified - rtf:CJK 转义、无关 info 项保留
- rtf:无 info 组时插入
- pdf:原字节是前缀、
/Info 4 0 R、/Prev - pdf:中文名
<FEFF...> - 分类器不把
.docx当.doc - 拒绝/支持清单分流
- 两个钩子注册;两次拒绝文案不同
- 系统用户名回退
allowMacro放行后仍落章- post-execute 端到端落章
- 磁盘落章后 mtime 不变
test/adversarial.mjs(34 项,边界):
数据描述符 / 归档注释 / store 条目 / 1 MiB 条目 / CD 额外字段 / 中文部件名 / BOM / XML 元字符 / 控制字符 / RTF 转义花括号 / xref 流 PDF / /Info 字段继承 / PDF 幂等 / 作者已正确时原样跳过(docx / pdf / rtf) / 加密 PDF / 混合引用 PDF / .docx 实为 OLE / 签名 ODF / 未知 zip / 空文件 / 大写扩展名 / 只读工具不触发 / 旧文件不触发 / 正文提及时不误拦门禁 / 带空格路径 / 相对路径 / 增补平面字符 / volatile 引用解包。
9. 扩展一种格式
- 新建
lib/adapters/x.js,导出id、extensions、stamp(buffer, meta)。 - 在
lib/adapters/index.js的ADAPTERS加一行。 - 在
test/run.mjs加 fixture 与至少两条断言(改对了 + 其余字节不变)。
10. 已知坑
- 命名空间声明在元素上:openpyxl 把
xmlns:dc写在<dc:creator>上;替换时丢属性会让 lxml 报Namespace prefix dc ... not defined。必须“只换文本、保留属性”。 - 数据描述符:改写条目要
flags & ~0x0008并写 sizes。 - EPUB 的裸
meta:用meta标签做 upsert 会误伤<meta name="cover">,只能插入。 - PDF
/Info在对象流里:重建会丢 Producer/Title,宁可跳过。 - utimes 必须在 rename 之后:顺序反了 mtime 会被改回“现在”。
- 文件名带空格:先取引号内完整片段,再退回按空白切分。
- PDF 重复落章:每次追加一节 xref,必须靠作者名幂等拦住。
- 锁定文件在读阶段就 EBUSY:读也要重试并映射成可识别的跳过原因。
- 门禁扫正文会误伤:写文档时提到旧扩展名不该被拦,扫描范围要按工具区分。
npm pack默认全收:必须用files白名单。
11. 术语
- OPC:Office Open XML 的包格式(zip +
[Content_Types].xml+ relationships)。 - CFB/OLE:旧版 Office 的复合文档容器。
- 增量更新:在 PDF 末尾追加一节并让
startxref指过去,原字节不变。 - 外科改写:只替换容器里的目标条目,其余字节原样复制。
许可
MIT
Related plugins
archify (deepseek-harness)
tt-a1i/archify
WeKnora (dsh-weknora)
tencent/weknora
weknora
tencent/weknora
BrowserSkill (dsh-plugin-browserskill)
tencent/browserskill