@rooode/dsh-plugin-preview 0.1.0
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 +121 -0
- package/cordis.patch.yml +16 -0
- package/lib/client.js +2200 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +212 -0
- package/package.json +68 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
3
|
+
|
|
4
|
+
export declare const name: string;
|
|
5
|
+
export declare const inject: string[];
|
|
6
|
+
export declare class PreviewService extends Service {
|
|
7
|
+
static inject: string[];
|
|
8
|
+
constructor(ctx: Context, config?: any);
|
|
9
|
+
}
|
|
10
|
+
export declare const Config: any;
|
|
11
|
+
export declare function apply(ctx: Context, config?: any): void;
|
|
12
|
+
declare const _default: {
|
|
13
|
+
name: string;
|
|
14
|
+
Config: any;
|
|
15
|
+
inject: string[];
|
|
16
|
+
apply: typeof apply;
|
|
17
|
+
};
|
|
18
|
+
export default _default;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
2
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { exec, execFile } from 'child_process';
|
|
6
|
+
|
|
7
|
+
export const name = 'ui-preview';
|
|
8
|
+
export const inject = ['webServer'];
|
|
9
|
+
|
|
10
|
+
export class PreviewService extends Service {
|
|
11
|
+
static inject = ['webServer'];
|
|
12
|
+
constructor(ctx, config = {}) {
|
|
13
|
+
super(ctx, 'preview');
|
|
14
|
+
this.config = {
|
|
15
|
+
autoPreviewExtensions: ['.md', '.markdown', '.txt', '.json', '.yaml', '.yml', '.js', '.ts', '.html', '.css'],
|
|
16
|
+
maxFileSizeMb: 10,
|
|
17
|
+
defaultViewMode: 'preview',
|
|
18
|
+
defaultPanelWidth: 560,
|
|
19
|
+
...config,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const webServer = this.ctx.get ? this.ctx.get('webServer') : this.ctx.webServer;
|
|
24
|
+
if (webServer && typeof webServer.register === 'function') {
|
|
25
|
+
this.ctx.effect(() => {
|
|
26
|
+
return webServer.register({
|
|
27
|
+
kind: 'prefix',
|
|
28
|
+
path: '/api/preview',
|
|
29
|
+
handler: this.handleHttpRequest.bind(this),
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
console.log('[PreviewService] Registered /api/preview route on webServer');
|
|
33
|
+
} else {
|
|
34
|
+
console.warn('[PreviewService] webServer not available yet in ctx');
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.warn('[PreviewService] Could not register route on webServer:', err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setCorsHeaders(res) {
|
|
42
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
43
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
44
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async parseRequestBody(req) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
let body = '';
|
|
50
|
+
req.on('data', chunk => { body += chunk; });
|
|
51
|
+
req.on('end', () => {
|
|
52
|
+
if (!body) return resolve({});
|
|
53
|
+
try { resolve(JSON.parse(body)); } catch (e) { resolve({ raw: body }); }
|
|
54
|
+
});
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
countWords(text) {
|
|
60
|
+
const cjk = (text.match(/[\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af]/g) || []).length;
|
|
61
|
+
const words = (text.replace(/[\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af]/g, ' ').match(/[a-zA-Z0-9_\-]+/g) || []).length;
|
|
62
|
+
return cjk + words;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
readFileInfo(targetPath) {
|
|
66
|
+
try {
|
|
67
|
+
const normalizedPath = path.resolve(targetPath);
|
|
68
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
69
|
+
return { ok: false, error: { code: 'FILE_NOT_FOUND', message: '文件不存在: ' + normalizedPath } };
|
|
70
|
+
}
|
|
71
|
+
const stat = fs.statSync(normalizedPath);
|
|
72
|
+
if (stat.isDirectory()) {
|
|
73
|
+
return { ok: false, error: { code: 'IS_DIRECTORY', message: '目标路径是目录' } };
|
|
74
|
+
}
|
|
75
|
+
const buffer = fs.readFileSync(normalizedPath);
|
|
76
|
+
const isBinary = buffer.slice(0, 1024).includes(0);
|
|
77
|
+
const content = isBinary ? '' : buffer.toString('utf-8');
|
|
78
|
+
const ext = path.extname(normalizedPath).toLowerCase();
|
|
79
|
+
const name = path.basename(normalizedPath);
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
file: {
|
|
83
|
+
path: normalizedPath,
|
|
84
|
+
displayPath: name,
|
|
85
|
+
name,
|
|
86
|
+
content,
|
|
87
|
+
size: stat.size,
|
|
88
|
+
mtime: stat.mtimeMs,
|
|
89
|
+
extension: ext,
|
|
90
|
+
lineCount: isBinary ? 0 : content.split('\n').length,
|
|
91
|
+
wordCount: isBinary ? 0 : this.countWords(content),
|
|
92
|
+
charCount: content.length,
|
|
93
|
+
isBinary,
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { ok: false, error: { code: 'READ_ERROR', message: err.message || '读取失败' } };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
revealInFileManager(targetPath) {
|
|
102
|
+
return new Promise(resolve => {
|
|
103
|
+
const normalizedPath = path.resolve(targetPath);
|
|
104
|
+
if (process.platform === 'win32') {
|
|
105
|
+
const winPath = normalizedPath.replace(/\//g, '\\');
|
|
106
|
+
if (fs.existsSync(winPath)) {
|
|
107
|
+
execFile('explorer.exe', ['/select,', winPath], () => resolve(true));
|
|
108
|
+
} else {
|
|
109
|
+
execFile('explorer.exe', [path.dirname(winPath)], () => resolve(true));
|
|
110
|
+
}
|
|
111
|
+
} else if (process.platform === 'darwin') {
|
|
112
|
+
execFile('open', ['-R', normalizedPath], () => resolve(true));
|
|
113
|
+
} else {
|
|
114
|
+
execFile('xdg-open', [path.dirname(normalizedPath)], () => resolve(true));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
openInNativeApp(targetPath) {
|
|
120
|
+
return new Promise(resolve => {
|
|
121
|
+
const normalizedPath = path.resolve(targetPath);
|
|
122
|
+
if (process.platform === 'win32') {
|
|
123
|
+
const winPath = normalizedPath.replace(/\//g, '\\');
|
|
124
|
+
exec('powershell.exe -NoProfile -Command "Invoke-Item -LiteralPath \'' + winPath.replace(/'/g, "''") + '\'"', () => resolve(true));
|
|
125
|
+
} else if (process.platform === 'darwin') {
|
|
126
|
+
execFile('open', [normalizedPath], () => resolve(true));
|
|
127
|
+
} else {
|
|
128
|
+
execFile('xdg-open', [normalizedPath], () => resolve(true));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async handleHttpRequest(req, res) {
|
|
134
|
+
this.setCorsHeaders(res);
|
|
135
|
+
if (req.method === 'OPTIONS') {
|
|
136
|
+
res.statusCode = 204;
|
|
137
|
+
res.end();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const url = new URL(req.url || '/', 'http://' + (req.headers.host || 'localhost'));
|
|
141
|
+
const pathname = url.pathname;
|
|
142
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
if (pathname === '/api/preview/status') {
|
|
146
|
+
res.statusCode = 200;
|
|
147
|
+
res.end(JSON.stringify({ ok: true, version: '0.1.0', service: 'preview' }));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (pathname === '/api/preview/read' && req.method === 'GET') {
|
|
151
|
+
const filePath = url.searchParams.get('path');
|
|
152
|
+
if (!filePath) {
|
|
153
|
+
res.statusCode = 400;
|
|
154
|
+
res.end(JSON.stringify({ ok: false, error: { code: 'MISSING_PARAM', message: '缺少 path 参数' } }));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const result = this.readFileInfo(filePath);
|
|
158
|
+
res.statusCode = result.ok ? 200 : 404;
|
|
159
|
+
res.end(JSON.stringify(result));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (pathname === '/api/preview/reveal' && req.method === 'POST') {
|
|
163
|
+
const body = await this.parseRequestBody(req);
|
|
164
|
+
if (!body.path) {
|
|
165
|
+
res.statusCode = 400;
|
|
166
|
+
res.end(JSON.stringify({ ok: false, error: { code: 'MISSING_PARAM', message: '缺少 path 参数' } }));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await this.revealInFileManager(body.path);
|
|
170
|
+
res.statusCode = 200;
|
|
171
|
+
res.end(JSON.stringify({ ok: true }));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (pathname === '/api/preview/open-native' && req.method === 'POST') {
|
|
175
|
+
const body = await this.parseRequestBody(req);
|
|
176
|
+
if (!body.path) {
|
|
177
|
+
res.statusCode = 400;
|
|
178
|
+
res.end(JSON.stringify({ ok: false, error: { code: 'MISSING_PARAM', message: '缺少 path 参数' } }));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await this.openInNativeApp(body.path);
|
|
182
|
+
res.statusCode = 200;
|
|
183
|
+
res.end(JSON.stringify({ ok: true }));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
res.statusCode = 404;
|
|
187
|
+
res.end(JSON.stringify({ ok: false, error: { code: 'NOT_FOUND', message: '未找到路由: ' + pathname } }));
|
|
188
|
+
} catch (e) {
|
|
189
|
+
res.statusCode = 500;
|
|
190
|
+
res.end(JSON.stringify({ ok: false, error: { code: 'INTERNAL_ERROR', message: e.message } }));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const Config = Schema.object({
|
|
196
|
+
autoPreviewExtensions: Schema.array(Schema.string()).default(['.md', '.markdown', '.txt', '.json', '.yaml', '.yml']),
|
|
197
|
+
maxFileSizeMb: Schema.number().default(10),
|
|
198
|
+
defaultViewMode: Schema.union(['preview', 'source', 'split']).default('preview'),
|
|
199
|
+
defaultPanelWidth: Schema.number().default(560),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
export function apply(ctx, config = {}) {
|
|
203
|
+
new PreviewService(ctx, config);
|
|
204
|
+
}
|
|
205
|
+
apply.inject = inject;
|
|
206
|
+
|
|
207
|
+
export default {
|
|
208
|
+
name,
|
|
209
|
+
Config,
|
|
210
|
+
inject,
|
|
211
|
+
apply,
|
|
212
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rooode/dsh-plugin-preview",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek Harness Markdown 文档右侧预览插件 (参考 WorkBuddy FileTabs 实现,支持多标签、GFM 渲染、TOC 大纲、源码分栏)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib",
|
|
10
|
+
"cordis.patch.yml",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "node scripts/build.js",
|
|
18
|
+
"dev": "node scripts/build.js --watch",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"package": "node scripts/build.js && npm pack",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"deepseek-harness",
|
|
25
|
+
"dsh",
|
|
26
|
+
"cordis-plugin",
|
|
27
|
+
"preview",
|
|
28
|
+
"markdown-preview",
|
|
29
|
+
"filetabs",
|
|
30
|
+
"workbuddy",
|
|
31
|
+
"viewer"
|
|
32
|
+
],
|
|
33
|
+
"author": "rooode",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=18.0.0",
|
|
37
|
+
"react-dom": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"typescript": "^5.4.0"
|
|
41
|
+
},
|
|
42
|
+
"dsh": {
|
|
43
|
+
"bundle": {
|
|
44
|
+
"patch": "./cordis.patch.yml"
|
|
45
|
+
},
|
|
46
|
+
"client": {
|
|
47
|
+
"platform": "web"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"react": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"react-dom": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"exports": {
|
|
59
|
+
".": {
|
|
60
|
+
"types": "./lib/index.d.ts",
|
|
61
|
+
"default": "./lib/index.js"
|
|
62
|
+
},
|
|
63
|
+
"./client": {
|
|
64
|
+
"default": "./lib/client.js"
|
|
65
|
+
},
|
|
66
|
+
"./package.json": "./package.json"
|
|
67
|
+
}
|
|
68
|
+
}
|