@liangjiehappy/dsh-ops 0.0.0-stage → 0.1.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/README.md +74 -2
- package/README.zh.md +74 -0
- package/cordis.patch.yml +24 -0
- package/lib/index.js +422 -0
- package/lib/skills.js +110 -0
- package/package.json +40 -3
package/README.md
CHANGED
|
@@ -1,3 +1,75 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @liangjiehappy/dsh-ops — DSH Ops Workbench
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[中文说明](README.zh.md)
|
|
4
|
+
|
|
5
|
+
A dev/ops workbench for DSH-managed sites. It checks health, SEO, GEO, code
|
|
6
|
+
and security, and does snapshot / deploy / rollback with automatic recovery.
|
|
7
|
+
|
|
8
|
+
**7 tools + 1 skill.**
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
dsh plugin --profile web add @liangjiehappy/dsh-ops
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then set your API key (get one at <https://dshio.org/me/>):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
dsh plugin config dsh-ops --set api_key=<your-key>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or set the environment variable `DSH_OPS_KEY`.
|
|
23
|
+
|
|
24
|
+
## Tools
|
|
25
|
+
|
|
26
|
+
| Tool | What it does | Mutates |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `ops_status` | Summary of all sites: score, per-category score, last deploy, open issues | no |
|
|
29
|
+
| `ops_check` | Run 15 checkers on one site, return detailed pass/warn/fail | no |
|
|
30
|
+
| `ops_issues` | Open issues, sorted by severity, with first-seen and count | no |
|
|
31
|
+
| `ops_snapshot` | Incremental snapshot (sha256 diff + hard links) | yes (add only) |
|
|
32
|
+
| `ops_deploy` | Deploy with impact analysis, auto-snapshot, auto-rollback | **yes** |
|
|
33
|
+
| `ops_rollback` | Restore to a snapshot | **yes** |
|
|
34
|
+
| `ops_restart` | Restart a systemd service | **yes** |
|
|
35
|
+
|
|
36
|
+
## Checkers (15)
|
|
37
|
+
|
|
38
|
+
| Category | Checkers |
|
|
39
|
+
|---|---|
|
|
40
|
+
| deploy | `deploy_smoke` `deploy_assets` `deploy_sitemap` |
|
|
41
|
+
| seo | `seo_title` `seo_desc` `seo_canonical` `seo_hreflang` `seo_structured` |
|
|
42
|
+
| geo | `geo_ai_crawler` `geo_citable` |
|
|
43
|
+
| code | `code_language_mix` `code_package_clean` `code_secret` |
|
|
44
|
+
| security | `sec_fail2ban` `sec_backup` |
|
|
45
|
+
|
|
46
|
+
## Skill
|
|
47
|
+
|
|
48
|
+
`ops-guide` — loaded when the user asks about site status, deployment or
|
|
49
|
+
rollback. It explains which tool to use, what to check before a risky
|
|
50
|
+
operation, and how to read the deploy impact analysis.
|
|
51
|
+
|
|
52
|
+
## Deploy safety — three layers
|
|
53
|
+
|
|
54
|
+
The `ops_deploy` tool was built in response to a real incident: an
|
|
55
|
+
`rsync --delete` removed 3 static JS files and broke mobile layout, the
|
|
56
|
+
feedback button and the PWA prompt for 4 days.
|
|
57
|
+
|
|
58
|
+
1. **Source completeness** — if the build directory is missing expected
|
|
59
|
+
subdirectories, deploy aborts (source is older than target).
|
|
60
|
+
2. **Impact analysis** — the tool lists every file that would be deleted.
|
|
61
|
+
Files not in the source *and* not on the delete whitelist abort the deploy.
|
|
62
|
+
3. **Verify + rollback** — a snapshot is taken before deploy; smoke checks run
|
|
63
|
+
after; any failure triggers automatic rollback.
|
|
64
|
+
|
|
65
|
+
Always run `ops_deploy` with `dry_run: true` first.
|
|
66
|
+
|
|
67
|
+
## Data source
|
|
68
|
+
|
|
69
|
+
API: `https://deepseekbase.com/work/api/`
|
|
70
|
+
|
|
71
|
+
Third-party project. Not affiliated with DeepSeek.
|
|
72
|
+
|
|
73
|
+
- Homepage: <https://deepseekbase.com/work/>
|
|
74
|
+
- Standards: <https://dshio.org/>
|
|
75
|
+
- License: MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @liangjiehappy/dsh-ops — DSH 運維工作台
|
|
2
|
+
|
|
3
|
+
[English](README.md)
|
|
4
|
+
|
|
5
|
+
跑在 DSH 裡的開發運維工作台。檢查站點的健康、SEO、GEO、代碼與安全,
|
|
6
|
+
並提供快照、部署、回滾,失敗自動恢復。
|
|
7
|
+
|
|
8
|
+
**7 個工具 + 1 個技能。**
|
|
9
|
+
|
|
10
|
+
## 安裝
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
dsh plugin --profile web add @liangjiehappy/dsh-ops
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
然後設定 API key(在 <https://dshio.org/me/> 生成):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
dsh plugin config dsh-ops --set api_key=<你的 key>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
或設定環境變數 `DSH_OPS_KEY`。
|
|
23
|
+
|
|
24
|
+
## 工具
|
|
25
|
+
|
|
26
|
+
| 工具 | 做什麼 | 會改系統嗎 |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `ops_status` | 所有站點摘要:總分、分類得分、最後部署、未解決問題數 | 否 |
|
|
29
|
+
| `ops_check` | 對單站跑 15 個檢查器,返回詳細通過/警告/失敗 | 否 |
|
|
30
|
+
| `ops_issues` | 未解決問題清單,按嚴重程度排序,含首次發現與出現次數 | 否 |
|
|
31
|
+
| `ops_snapshot` | 增量快照(sha256 對比 + 硬鏈接)| 是(只增不刪)|
|
|
32
|
+
| `ops_deploy` | 部署,含影響分析、自動快照、驗證失敗自動回滾 | **是** |
|
|
33
|
+
| `ops_rollback` | 回滾到指定快照 | **是** |
|
|
34
|
+
| `ops_restart` | 重啟 systemd 服務 | **是** |
|
|
35
|
+
|
|
36
|
+
## 檢查器(15 個)
|
|
37
|
+
|
|
38
|
+
| 類別 | 檢查器 |
|
|
39
|
+
|---|---|
|
|
40
|
+
| 部署 | `deploy_smoke` `deploy_assets` `deploy_sitemap` |
|
|
41
|
+
| SEO | `seo_title` `seo_desc` `seo_canonical` `seo_hreflang` `seo_structured` |
|
|
42
|
+
| GEO | `geo_ai_crawler` `geo_citable` |
|
|
43
|
+
| 代碼 | `code_language_mix` `code_package_clean` `code_secret` |
|
|
44
|
+
| 安全 | `sec_fail2ban` `sec_backup` |
|
|
45
|
+
|
|
46
|
+
每一個都是從真實事故裡長出來的:
|
|
47
|
+
|
|
48
|
+
- `deploy_assets` ← 一次 `rsync --delete` 刪掉 3 個靜態 JS,手機適配、反饋按鈕、PWA 提示全部失效 4 天
|
|
49
|
+
- `code_language_mix` ← 中文插件裡混進了 6 行荷蘭語
|
|
50
|
+
- `code_package_clean` ← `.bak` 備份文件被發上了 npm
|
|
51
|
+
- `deploy_sitemap` ← sitemap 超過 5 萬 URL 上限
|
|
52
|
+
|
|
53
|
+
## 技能
|
|
54
|
+
|
|
55
|
+
`ops-guide` —— 當使用者問站點狀態、要部署、要回滾時載入。
|
|
56
|
+
它說明該用哪個工具、危險操作前該確認什麼、部署影響分析怎麼讀。
|
|
57
|
+
|
|
58
|
+
## 部署的三層保護
|
|
59
|
+
|
|
60
|
+
1. **源頭完整性** —— build 目錄缺少預期子目錄時中止(說明上次構建不完整)
|
|
61
|
+
2. **影響分析** —— 列出所有會被刪的文件;既有源頭沒有、又不在刪除白名單的,中止部署
|
|
62
|
+
3. **驗證 + 回滾** —— 部署前自動快照;部署後跑 smoke 檢查;失敗自動回滾
|
|
63
|
+
|
|
64
|
+
**強烈建議先用 `dry_run: true` 跑一次看影響分析。**
|
|
65
|
+
|
|
66
|
+
## 數據來源
|
|
67
|
+
|
|
68
|
+
API:`https://deepseekbase.com/work/api/`
|
|
69
|
+
|
|
70
|
+
第三方非官方項目,與 DeepSeek 官方無關。
|
|
71
|
+
|
|
72
|
+
- 首頁:<https://deepseekbase.com/work/>
|
|
73
|
+
- 標準:<https://dshio.org/>
|
|
74
|
+
- 許可:MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# DSH 運維工作台
|
|
2
|
+
#
|
|
3
|
+
# 本包提供:
|
|
4
|
+
# 工具(7 個):
|
|
5
|
+
# ops_status 所有站点状态摘要
|
|
6
|
+
# ops_check 跑全部检查器
|
|
7
|
+
# ops_issues 未解决的问题
|
|
8
|
+
# ops_snapshot 打快照
|
|
9
|
+
# ops_deploy 部署(含影响分析 + 自动回滚)
|
|
10
|
+
# ops_rollback 回滚到快照
|
|
11
|
+
# ops_restart 重启服务
|
|
12
|
+
#
|
|
13
|
+
# 技能(1 個):
|
|
14
|
+
# ops-guide 运维工作台使用指南(何时用哪个工具、危险操作注意什么)
|
|
15
|
+
#
|
|
16
|
+
# 依赖:
|
|
17
|
+
# 工作台后端 https://deepseekbase.com/work/api/
|
|
18
|
+
# 认证复用 dshio API key(在 https://dshio.org/me/ 生成)
|
|
19
|
+
#
|
|
20
|
+
# 本包不写本地文件、不执行本地命令;
|
|
21
|
+
# 所有动作通过 HTTPS 调工作台 API 执行。
|
|
22
|
+
- insert:
|
|
23
|
+
- id: dsh-ops
|
|
24
|
+
name: "@liangjiehappy/dsh-ops"
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH 運維工作台 —— 主入口
|
|
3
|
+
*
|
|
4
|
+
* 按 DSH 官方插件标准编写:
|
|
5
|
+
* · ESM,export const name / inject / apply
|
|
6
|
+
* · ctx.tools.register(defineTool({...}))
|
|
7
|
+
* · ctx.skills.register({...})
|
|
8
|
+
*
|
|
9
|
+
* 工具(7 個):
|
|
10
|
+
* ops_status 所有站点状态摘要
|
|
11
|
+
* ops_check 跑检查器
|
|
12
|
+
* ops_issues 未解决问题
|
|
13
|
+
* ops_snapshot 打快照
|
|
14
|
+
* ops_deploy 部署(含影响分析)
|
|
15
|
+
* ops_rollback 回滚
|
|
16
|
+
* ops_restart 重启服务
|
|
17
|
+
*
|
|
18
|
+
* 技能(1 個):
|
|
19
|
+
* ops-guide 工作台使用指南
|
|
20
|
+
*
|
|
21
|
+
* 数据来自工作台后端 https://deepseekbase.com/work/api/
|
|
22
|
+
* 认证用 dshio API key(https://dshio.org/me/ 生成)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
26
|
+
import { ALL_SKILLS } from "./skills.js";
|
|
27
|
+
|
|
28
|
+
const BASE = "https://deepseekbase.com/work/api";
|
|
29
|
+
const TIMEOUT_MS = 60000; // 部署可能要几十秒
|
|
30
|
+
|
|
31
|
+
export const name = "dsh-ops";
|
|
32
|
+
export const inject = ["tools", "skills"];
|
|
33
|
+
|
|
34
|
+
const TEXT_OUTPUT = { type: "text" };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 调工作台 API。
|
|
38
|
+
* 认证优先用 config.api_key,其次环境变量 DSH_OPS_KEY。
|
|
39
|
+
*/
|
|
40
|
+
async function api(path, opts = {}, cfg = {}, signal) {
|
|
41
|
+
const key =
|
|
42
|
+
(cfg && cfg.api_key) ||
|
|
43
|
+
(typeof process !== "undefined" && process.env && process.env.DSH_OPS_KEY) ||
|
|
44
|
+
"";
|
|
45
|
+
const headers = { "Content-Type": "application/json" };
|
|
46
|
+
if (key) headers["Authorization"] = "Bearer " + key;
|
|
47
|
+
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
50
|
+
if (signal) {
|
|
51
|
+
if (signal.aborted) controller.abort();
|
|
52
|
+
else signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(BASE + path, {
|
|
57
|
+
method: opts.method || "GET",
|
|
58
|
+
headers,
|
|
59
|
+
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
});
|
|
62
|
+
let data = null;
|
|
63
|
+
const raw = await res.text();
|
|
64
|
+
try {
|
|
65
|
+
data = JSON.parse(raw);
|
|
66
|
+
} catch {
|
|
67
|
+
data = { raw: raw.slice(0, 800) };
|
|
68
|
+
}
|
|
69
|
+
if (res.status === 401 || res.status === 403) {
|
|
70
|
+
return {
|
|
71
|
+
error:
|
|
72
|
+
"API key 无效或缺失。\n" +
|
|
73
|
+
"去 https://dshio.org/me/ 生成 key,然后:\n" +
|
|
74
|
+
" dsh plugin config dsh-ops --set api_key=<你的 key>",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
return { error: `HTTP ${res.status}: ${(data && (data.error || data.detail)) || raw.slice(0, 200)}` };
|
|
79
|
+
}
|
|
80
|
+
return { data };
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (e.name === "AbortError") {
|
|
83
|
+
return { error: "工作台超时(" + Math.round(TIMEOUT_MS / 1000) + " 秒)。部署可能仍在后台执行,稍后用 ops_status 查看。" };
|
|
84
|
+
}
|
|
85
|
+
return { error: "无法连到工作台:" + String(e.message || e).slice(0, 160) };
|
|
86
|
+
} finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function apply(ctx, config = {}) {
|
|
92
|
+
const cfg = config || {};
|
|
93
|
+
|
|
94
|
+
// ═══════════════════════════════════════════════════════════════
|
|
95
|
+
// 1. ops_status —— 所有站点状态摘要
|
|
96
|
+
// ═══════════════════════════════════════════════════════════════
|
|
97
|
+
ctx.tools.register(
|
|
98
|
+
defineTool({
|
|
99
|
+
name: "ops_status",
|
|
100
|
+
description:
|
|
101
|
+
"查看运维工作台管理的所有站点的当前状态:总分、分类得分、最近部署时间、未解决问题数。" +
|
|
102
|
+
"当用户问「站现在怎么样」「有没有挂」「体检一下」时使用。只读,不改任何东西。",
|
|
103
|
+
parameters: {},
|
|
104
|
+
output: TEXT_OUTPUT,
|
|
105
|
+
async execute(args, exec) {
|
|
106
|
+
const r = await api("/status", {}, cfg, exec && exec.signal);
|
|
107
|
+
if (r.error) return { text: r.error };
|
|
108
|
+
const d = r.data || {};
|
|
109
|
+
const lines = [];
|
|
110
|
+
if (d.checked_at) lines.push("检查时间: " + d.checked_at);
|
|
111
|
+
lines.push("");
|
|
112
|
+
for (const p of d.projects || []) {
|
|
113
|
+
const mark = p.score >= 90 ? "✓" : p.score >= 70 ? "⚠" : "✗";
|
|
114
|
+
lines.push(
|
|
115
|
+
`${String(p.slug || "").padEnd(22)} ${String(p.product_type || "web").padEnd(6)} ` +
|
|
116
|
+
`${String(p.score).padStart(3)} 分 ${mark}`
|
|
117
|
+
);
|
|
118
|
+
if (p.by_category) {
|
|
119
|
+
const cats = Object.entries(p.by_category)
|
|
120
|
+
.map(([k, v]) => `${k} ${v}`)
|
|
121
|
+
.join(" · ");
|
|
122
|
+
if (cats) lines.push(" · " + cats);
|
|
123
|
+
}
|
|
124
|
+
if (p.last_deploy) {
|
|
125
|
+
lines.push(" · 最后部署 " + p.last_deploy);
|
|
126
|
+
}
|
|
127
|
+
if (p.open_issues) {
|
|
128
|
+
lines.push(" · " + p.open_issues + " 个未解决问题");
|
|
129
|
+
}
|
|
130
|
+
lines.push("");
|
|
131
|
+
}
|
|
132
|
+
if (!d.projects || !d.projects.length) {
|
|
133
|
+
lines.push("工作台还没有注册任何项目。");
|
|
134
|
+
}
|
|
135
|
+
return { text: lines.join("\n") };
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// ═══════════════════════════════════════════════════════════════
|
|
141
|
+
// 2. ops_check —— 跑检查器
|
|
142
|
+
// ═══════════════════════════════════════════════════════════════
|
|
143
|
+
ctx.tools.register(
|
|
144
|
+
defineTool({
|
|
145
|
+
name: "ops_check",
|
|
146
|
+
description:
|
|
147
|
+
"对某个站点跑检查器(15 个),返回通过/警告/失败的详细结果。" +
|
|
148
|
+
"当用户说「检查一下 XX 站」「SEO 有问题吗」「静态资源有没有缺」时使用。" +
|
|
149
|
+
"只读,不改任何东西。不传 checker 就跑全部。",
|
|
150
|
+
parameters: {
|
|
151
|
+
project: {
|
|
152
|
+
type: "string",
|
|
153
|
+
required: true,
|
|
154
|
+
description: "项目 slug,如 oracleluck / deepseekbase / dshio",
|
|
155
|
+
},
|
|
156
|
+
checker: {
|
|
157
|
+
type: "string",
|
|
158
|
+
description:
|
|
159
|
+
"只跑指定检查器。可选:deploy_smoke / deploy_assets / deploy_sitemap / " +
|
|
160
|
+
"seo_title / seo_desc / seo_canonical / seo_hreflang / seo_structured / " +
|
|
161
|
+
"geo_ai_crawler / geo_citable / code_language_mix / code_package_clean / " +
|
|
162
|
+
"code_secret / sec_fail2ban / sec_backup",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
output: TEXT_OUTPUT,
|
|
166
|
+
async execute(args, exec) {
|
|
167
|
+
const q = args.checker ? "?checker=" + encodeURIComponent(args.checker) : "";
|
|
168
|
+
const r = await api(
|
|
169
|
+
"/projects/" + encodeURIComponent(args.project) + "/check" + q,
|
|
170
|
+
{ method: "POST", body: {} },
|
|
171
|
+
cfg,
|
|
172
|
+
exec && exec.signal
|
|
173
|
+
);
|
|
174
|
+
if (r.error) return { text: r.error };
|
|
175
|
+
const d = r.data || {};
|
|
176
|
+
const lines = [];
|
|
177
|
+
lines.push(`项目: ${d.project || args.project}`);
|
|
178
|
+
lines.push(`总分: ${d.score} 通过 ${d.passed} 警告 ${d.warned} 失败 ${d.failed} 错误 ${d.errored}`);
|
|
179
|
+
lines.push("");
|
|
180
|
+
for (const c of d.results || []) {
|
|
181
|
+
const mark = c.status === "pass" ? "✓" : c.status === "warn" ? "⚠" : c.status === "error" ? "!" : "✗";
|
|
182
|
+
lines.push(` ${mark} ${String(c.checker_id).padEnd(22)} ${String(c.score).padStart(5)} 分`);
|
|
183
|
+
if (c.issues && c.issues.length) {
|
|
184
|
+
for (const i of c.issues.slice(0, 5)) {
|
|
185
|
+
lines.push(` [${i.level}] ${i.title}`);
|
|
186
|
+
if (i.fix_hint) lines.push(` 修: ${i.fix_hint}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { text: lines.join("\n") };
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// ═══════════════════════════════════════════════════════════════
|
|
196
|
+
// 3. ops_issues —— 未解决问题
|
|
197
|
+
// ═══════════════════════════════════════════════════════════════
|
|
198
|
+
ctx.tools.register(
|
|
199
|
+
defineTool({
|
|
200
|
+
name: "ops_issues",
|
|
201
|
+
description:
|
|
202
|
+
"列出某个站点未解决的问题,按严重程度排序,含首次发现时间和出现次数。" +
|
|
203
|
+
"当用户问「还有什么没修」「问题清单」「反复出现的问题」时使用。只读。",
|
|
204
|
+
parameters: {
|
|
205
|
+
project: { type: "string", required: true, description: "项目 slug" },
|
|
206
|
+
level: {
|
|
207
|
+
type: "string",
|
|
208
|
+
description: "只列指定级别:info / warn / critical",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
output: TEXT_OUTPUT,
|
|
212
|
+
async execute(args, exec) {
|
|
213
|
+
const q = args.level ? "?level=" + encodeURIComponent(args.level) : "";
|
|
214
|
+
const r = await api(
|
|
215
|
+
"/projects/" + encodeURIComponent(args.project) + "/issues" + q,
|
|
216
|
+
{},
|
|
217
|
+
cfg,
|
|
218
|
+
exec && exec.signal
|
|
219
|
+
);
|
|
220
|
+
if (r.error) return { text: r.error };
|
|
221
|
+
const d = r.data || {};
|
|
222
|
+
const items = d.issues || [];
|
|
223
|
+
if (!items.length) return { text: "没有未解决的问题。" };
|
|
224
|
+
const lines = [`未解决问题: ${items.length} 个`, ""];
|
|
225
|
+
const mark = { critical: "✗", warn: "⚠", info: "·" };
|
|
226
|
+
for (const i of items) {
|
|
227
|
+
lines.push(`${mark[i.level] || "·"} [${i.level}] ${i.title}`);
|
|
228
|
+
if (i.fix_hint) lines.push(" 修: " + i.fix_hint);
|
|
229
|
+
lines.push(` 首次 ${i.first_seen} · 出现 ${i.seen_count} 次`);
|
|
230
|
+
}
|
|
231
|
+
return { text: lines.join("\n") };
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════
|
|
237
|
+
// 4. ops_snapshot —— 打快照
|
|
238
|
+
// ═══════════════════════════════════════════════════════════════
|
|
239
|
+
ctx.tools.register(
|
|
240
|
+
defineTool({
|
|
241
|
+
name: "ops_snapshot",
|
|
242
|
+
description:
|
|
243
|
+
"给某个站点当前状态打一份增量快照(只存变化的文件,未变的硬链接)。" +
|
|
244
|
+
"部署前、改配置前应该先打。当用户说「先备份一下」「打个快照」时使用。" +
|
|
245
|
+
"只增不删,安全。",
|
|
246
|
+
parameters: {
|
|
247
|
+
project: { type: "string", required: true, description: "项目 slug" },
|
|
248
|
+
label: { type: "string", description: "快照标签,如 before-seo-change" },
|
|
249
|
+
},
|
|
250
|
+
output: TEXT_OUTPUT,
|
|
251
|
+
async execute(args, exec) {
|
|
252
|
+
const r = await api(
|
|
253
|
+
"/projects/" + encodeURIComponent(args.project) + "/snapshot",
|
|
254
|
+
{ method: "POST", body: { label: args.label || "" } },
|
|
255
|
+
cfg,
|
|
256
|
+
exec && exec.signal
|
|
257
|
+
);
|
|
258
|
+
if (r.error) return { text: r.error };
|
|
259
|
+
const d = r.data || {};
|
|
260
|
+
return {
|
|
261
|
+
text:
|
|
262
|
+
`快照完成\n` +
|
|
263
|
+
` id: ${d.snapshot_id}\n` +
|
|
264
|
+
` 文件: ${d.files}(新增 ${d.new} / 变化 ${d.changed} / 未变 ${d.linked})\n` +
|
|
265
|
+
` 大小: ${(d.size_bytes / 1048576).toFixed(1)} MB\n` +
|
|
266
|
+
` 耗时: ${d.duration_sec} 秒`,
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// ═══════════════════════════════════════════════════════════════
|
|
273
|
+
// 5. ops_deploy —— 部署(含影响分析)
|
|
274
|
+
// ═══════════════════════════════════════════════════════════════
|
|
275
|
+
ctx.tools.register(
|
|
276
|
+
defineTool({
|
|
277
|
+
name: "ops_deploy",
|
|
278
|
+
description:
|
|
279
|
+
"部署某个站点到生产。内置三层保护:部署前影响分析(会删除哪些文件)、" +
|
|
280
|
+
"自动快照、部署后验证,验证失败自动回滚。" +
|
|
281
|
+
"当用户说「部署」「上线」「发布」时使用。" +
|
|
282
|
+
"强烈建议先用 dry_run=true 看影响分析。",
|
|
283
|
+
parameters: {
|
|
284
|
+
project: { type: "string", required: true, description: "项目 slug" },
|
|
285
|
+
dry_run: {
|
|
286
|
+
type: "boolean",
|
|
287
|
+
description: "true 只看影响分析不执行。强烈建议先跑一次。",
|
|
288
|
+
},
|
|
289
|
+
render: {
|
|
290
|
+
type: "boolean",
|
|
291
|
+
description: "是否重新生成页面,默认 true。只同步不渲染时设 false。",
|
|
292
|
+
},
|
|
293
|
+
force: {
|
|
294
|
+
type: "boolean",
|
|
295
|
+
description:
|
|
296
|
+
"强制部署,跳过「删除白名单」保护。只有确认被删文件无害时才用。",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
output: TEXT_OUTPUT,
|
|
300
|
+
isConcurrencySafe: () => false,
|
|
301
|
+
async execute(args, exec) {
|
|
302
|
+
const r = await api(
|
|
303
|
+
"/projects/" + encodeURIComponent(args.project) + "/deploy",
|
|
304
|
+
{
|
|
305
|
+
method: "POST",
|
|
306
|
+
body: {
|
|
307
|
+
dry_run: !!args.dry_run,
|
|
308
|
+
render: args.render !== false,
|
|
309
|
+
force: !!args.force,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
cfg,
|
|
313
|
+
exec && exec.signal
|
|
314
|
+
);
|
|
315
|
+
if (r.error) return { text: r.error };
|
|
316
|
+
const d = r.data || {};
|
|
317
|
+
return { text: d.output || JSON.stringify(d, null, 2) };
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// ═══════════════════════════════════════════════════════════════
|
|
323
|
+
// 6. ops_rollback —— 回滚
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════
|
|
325
|
+
ctx.tools.register(
|
|
326
|
+
defineTool({
|
|
327
|
+
name: "ops_rollback",
|
|
328
|
+
description:
|
|
329
|
+
"把某个站点回滚到指定快照。不传 snapshot_id 就用最近一个快照。" +
|
|
330
|
+
"当部署后发现问题、或用户说「回滚」「退回上一个版本」时使用。" +
|
|
331
|
+
"回滚也会删除快照里没有的文件,建议先 dry_run=true 看影响。",
|
|
332
|
+
parameters: {
|
|
333
|
+
project: { type: "string", required: true, description: "项目 slug" },
|
|
334
|
+
snapshot_id: {
|
|
335
|
+
type: "number",
|
|
336
|
+
description: "要回滚到的快照 id。不传则用最近一个。",
|
|
337
|
+
},
|
|
338
|
+
dry_run: {
|
|
339
|
+
type: "boolean",
|
|
340
|
+
description: "true 只看影响分析不执行。",
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
output: TEXT_OUTPUT,
|
|
344
|
+
isConcurrencySafe: () => false,
|
|
345
|
+
async execute(args, exec) {
|
|
346
|
+
const r = await api(
|
|
347
|
+
"/projects/" + encodeURIComponent(args.project) + "/rollback",
|
|
348
|
+
{
|
|
349
|
+
method: "POST",
|
|
350
|
+
body: {
|
|
351
|
+
snapshot_id: args.snapshot_id || null,
|
|
352
|
+
dry_run: !!args.dry_run,
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
cfg,
|
|
356
|
+
exec && exec.signal
|
|
357
|
+
);
|
|
358
|
+
if (r.error) return { text: r.error };
|
|
359
|
+
const d = r.data || {};
|
|
360
|
+
return { text: d.output || JSON.stringify(d, null, 2) };
|
|
361
|
+
},
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// ═══════════════════════════════════════════════════════════════
|
|
366
|
+
// 7. ops_restart —— 重启服务
|
|
367
|
+
// ═══════════════════════════════════════════════════════════════
|
|
368
|
+
ctx.tools.register(
|
|
369
|
+
defineTool({
|
|
370
|
+
name: "ops_restart",
|
|
371
|
+
description:
|
|
372
|
+
"重启某个站点的服务(如 Django CMS)。改了模板或配置要生效时用。" +
|
|
373
|
+
"当用户说「重启」「改完不生效」时使用。重启期间会有几秒 502。",
|
|
374
|
+
parameters: {
|
|
375
|
+
project: { type: "string", required: true, description: "项目 slug" },
|
|
376
|
+
service: {
|
|
377
|
+
type: "string",
|
|
378
|
+
description: "服务名,如 oracleluck-cms。不传则用项目默认服务。",
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
output: TEXT_OUTPUT,
|
|
382
|
+
isConcurrencySafe: () => false,
|
|
383
|
+
async execute(args, exec) {
|
|
384
|
+
const r = await api(
|
|
385
|
+
"/projects/" + encodeURIComponent(args.project) + "/restart",
|
|
386
|
+
{ method: "POST", body: { service: args.service || "" } },
|
|
387
|
+
cfg,
|
|
388
|
+
exec && exec.signal
|
|
389
|
+
);
|
|
390
|
+
if (r.error) return { text: r.error };
|
|
391
|
+
const d = r.data || {};
|
|
392
|
+
return { text: d.output || JSON.stringify(d, null, 2) };
|
|
393
|
+
},
|
|
394
|
+
})
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// ═══════════════════════════════════════════════════════════════
|
|
398
|
+
// 技能注册
|
|
399
|
+
// ═══════════════════════════════════════════════════════════════
|
|
400
|
+
if (ctx.skills && typeof ctx.skills.register === "function") {
|
|
401
|
+
for (const sk of ALL_SKILLS) {
|
|
402
|
+
try {
|
|
403
|
+
ctx.skills.register({
|
|
404
|
+
name: sk.name,
|
|
405
|
+
description: sk.description,
|
|
406
|
+
content: sk.content,
|
|
407
|
+
source: "runtime",
|
|
408
|
+
whenToUse: sk.whenToUse,
|
|
409
|
+
metadata: {
|
|
410
|
+
source: "dsh-ops",
|
|
411
|
+
layer: "bundled",
|
|
412
|
+
version: "0.1.0",
|
|
413
|
+
lang: "zh-Hans",
|
|
414
|
+
homepage: "https://deepseekbase.com/work/",
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
} catch (e) {
|
|
418
|
+
// 技能注册失败不影响工具使用
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
package/lib/skills.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// DSH 运维工作台 —— 技能内容
|
|
2
|
+
//
|
|
3
|
+
// 技能是给模型看的:告诉它什么时候该用哪个工具、哪些操作危险。
|
|
4
|
+
// 与工具的区别:工具会执行,技能只是知识。
|
|
5
|
+
|
|
6
|
+
export const OPS_GUIDE_SKILL = {
|
|
7
|
+
name: "ops-guide",
|
|
8
|
+
description:
|
|
9
|
+
"DSH 運維工作台使用指南。说明 ops_status / ops_check / ops_deploy / " +
|
|
10
|
+
"ops_rollback 等工具何时该用、危险操作前该确认什么、部署影响分析怎么看。" +
|
|
11
|
+
"当用户问「站现在怎么样」「能不能部署」「出问题了怎么办」时载入本技能。",
|
|
12
|
+
whenToUse:
|
|
13
|
+
"用户询问站点状态、要部署、要回滚、要排查线上问题时使用。" +
|
|
14
|
+
"纯内容问题(写文章、改文案)不需要本技能。",
|
|
15
|
+
content: `# DSH 運維工作台 使用指南
|
|
16
|
+
|
|
17
|
+
工作台管 3 个站点:\`oracleluck.com\`(玄学)、\`deepseekbase.com\`(DSH 插件市场)、\`dshio.org\`(标准机构)。
|
|
18
|
+
|
|
19
|
+
## 一、四个工具的分工
|
|
20
|
+
|
|
21
|
+
| 工具 | 什么时候用 | 会改系统吗 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| \`ops_status\` | 用户问「站怎么样」「有没有问题」 | 否 |
|
|
24
|
+
| \`ops_check\` | 要详细检查某一项或全部 | 否 |
|
|
25
|
+
| \`ops_snapshot\` | 部署前、改配置前 | 是(但只增不删) |
|
|
26
|
+
| \`ops_deploy\` | 要上线改动 | **是,高风险** |
|
|
27
|
+
| \`ops_rollback\` | 部署后发现问题 | **是,高风险** |
|
|
28
|
+
| \`ops_restart\` | 改了模板/配置要生效 | **是,中风险** |
|
|
29
|
+
|
|
30
|
+
## 二、危险操作的规矩
|
|
31
|
+
|
|
32
|
+
### ops_deploy 前必须做
|
|
33
|
+
|
|
34
|
+
1. **先跑 \`ops_deploy --dry-run\`** —— 看影响分析
|
|
35
|
+
2. 读懂「将删除 N 个文件」这段
|
|
36
|
+
3. 如果出现「不在源头且不在白名单」的文件,**不要加 --force**,先问用户
|
|
37
|
+
|
|
38
|
+
**真实事故背景:** 2026-11-09 一次 \`rsync --delete\` 删掉了 3 个静态 JS 文件,导致整个站的手机适配、反馈按钮、PWA 提示全部失效 4 天。工作台的影响分析就是为这个事故加的。
|
|
39
|
+
|
|
40
|
+
### ops_rollback 前必须做
|
|
41
|
+
|
|
42
|
+
1. 先 \`ops_rollback --dry-run\`,看会恢复多少、删除多少
|
|
43
|
+
2. 确认快照 id 是想要的(\`ops_status\` 里有最近 5 个快照)
|
|
44
|
+
3. 回滚也会删除文件(快照里没有的),确认后再执行
|
|
45
|
+
|
|
46
|
+
### ops_restart 的影响
|
|
47
|
+
|
|
48
|
+
- \`oracleluck-cms\` 重启约 3-5 秒,期间请求会 502
|
|
49
|
+
- 不要在流量高峰做
|
|
50
|
+
|
|
51
|
+
## 三、检查器清单(15 个)
|
|
52
|
+
|
|
53
|
+
**部署类**
|
|
54
|
+
- \`deploy_smoke\` 关键页可用性
|
|
55
|
+
- \`deploy_assets\` 静态资源存在性(这条抓的就是 11-09 那个事故)
|
|
56
|
+
- \`deploy_sitemap\` sitemap 合规(5 万 URL / 50MB 上限)
|
|
57
|
+
|
|
58
|
+
**SEO 类**
|
|
59
|
+
- \`seo_title\` 标题(长度、核心词、模板词)
|
|
60
|
+
- \`seo_desc\` 描述
|
|
61
|
+
- \`seo_canonical\` canonical 自指
|
|
62
|
+
- \`seo_hreflang\` hreflang 对称
|
|
63
|
+
- \`seo_structured\` JSON-LD 结构化数据
|
|
64
|
+
|
|
65
|
+
**GEO 类**
|
|
66
|
+
- \`geo_ai_crawler\` AI 爬虫放行(robots.txt)
|
|
67
|
+
- \`geo_citable\` 内容可摘录性
|
|
68
|
+
|
|
69
|
+
**代码类**
|
|
70
|
+
- \`code_language_mix\` 多语言混入(抓过荷兰语混进中文站的事故)
|
|
71
|
+
- \`code_package_clean\` 打包清洁(抓过 .bak 被发上 npm 的事故)
|
|
72
|
+
- \`code_secret\` 敏感信息泄漏
|
|
73
|
+
|
|
74
|
+
**安全类**
|
|
75
|
+
- \`sec_fail2ban\` 防爆破
|
|
76
|
+
- \`sec_backup\` 备份完整性
|
|
77
|
+
|
|
78
|
+
## 四、输出怎么读
|
|
79
|
+
|
|
80
|
+
\`\`\`
|
|
81
|
+
deepseekbase.com web 92 分 ✓
|
|
82
|
+
· SEO 98 ✓ · GEO 80 ⚠ · 安全 95 ✓
|
|
83
|
+
· 部署 3 小时前,smoke 全过
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
- ✓ 全部通过
|
|
87
|
+
- ⚠ 有警告(不影响使用,但建议修)
|
|
88
|
+
- ✗ 有失败(会影响用户)
|
|
89
|
+
|
|
90
|
+
## 五、什么时候不该用工作台
|
|
91
|
+
|
|
92
|
+
| 场景 | 该用什么 |
|
|
93
|
+
|---|---|
|
|
94
|
+
| 改文案、写文章 | 直接编辑,不用工作台 |
|
|
95
|
+
| 查数据 | 查数据库,不用工作台 |
|
|
96
|
+
| 检查 DSH 插件合规 | \`dshio-check\` 技能 + \`deepseekbase_check\` 工具 |
|
|
97
|
+
| 查模型价格 | \`deepseekbase_price\` 工具 |
|
|
98
|
+
|
|
99
|
+
## 六、故障时怎么排查
|
|
100
|
+
|
|
101
|
+
1. \`ops_status\` —— 先看哪站红
|
|
102
|
+
2. \`ops_check <project>\` —— 看具体哪个检查器失败
|
|
103
|
+
3. \`ops_issues <project>\` —— 看历史问题,是不是反复出现
|
|
104
|
+
4. 如果是刚部署后出的问题 → \`ops_rollback\`
|
|
105
|
+
|
|
106
|
+
**不要跳过第 1 步直接回滚** —— 可能问题不在代码,而在服务器或 DNS。
|
|
107
|
+
`,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const ALL_SKILLS = [OPS_GUIDE_SKILL];
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liangjiehappy/dsh-ops",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DSH 運維工作台:檢查 3 個站點的健康、SEO、GEO、代碼與安全,支援快照、部署與自動回滾。當使用者問「站現在怎麼樣」「部署安全嗎」「幫我檢查一下」時使用。",
|
|
5
|
+
"description_en": "DSH operations workbench: check health, SEO, GEO, code and security for DSH-managed sites, with snapshot, deploy and automatic rollback.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib",
|
|
10
|
+
"cordis.patch.yml",
|
|
11
|
+
"README.md",
|
|
12
|
+
"README.zh.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"dsh-plugin",
|
|
16
|
+
"dsh",
|
|
17
|
+
"devops",
|
|
18
|
+
"operations",
|
|
19
|
+
"monitoring",
|
|
20
|
+
"deploy",
|
|
21
|
+
"snapshot",
|
|
22
|
+
"rollback",
|
|
23
|
+
"skills"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://deepseekbase.com/work/",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/moldkey/dsh-deepseekbase.git",
|
|
30
|
+
"directory": "sites/dsh-ops"
|
|
31
|
+
},
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"dsh": ">=0.2.0-rc.2"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@deepseek-ai/dsh-tools": "*"
|
|
42
|
+
}
|
|
6
43
|
}
|