@stylewx/mcp-server 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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,257 @@
1
+ /**
2
+ * stylewx MCP Server 入口。
3
+ * 双传输模式:--transport stdio(默认) | --transport http。
4
+ * 依赖注入:从环境变量构造 LLM / 微信客户端;缺少时对应 tool 会返回明确错误,而非崩溃。
5
+ */
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
+ import { createServer } from 'node:http';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { readFileSync } from 'node:fs';
11
+ import { resolve, dirname } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { createMcpServer } from './server.js';
14
+ import { loadLlmConfigFromEnv, LlmClient, listThemes, listSavedThemes, renderPreview, generateTheme, saveTheme, exportTheme, validateArticle, resolveTheme, optimizeArticle, asServiceError, } from '@stylewx/service';
15
+ import { loadConfigFromEnv, WeChatClient, publishDraft as publisherPublishDraft } from '@stylewx/publisher';
16
+ function parseArgs(argv) {
17
+ let transport = 'stdio';
18
+ let port = 3000;
19
+ for (let i = 0; i < argv.length; i += 1) {
20
+ const arg = argv[i];
21
+ if (arg === '--transport') {
22
+ const next = argv[i + 1];
23
+ if (next === 'http' || next === 'stdio')
24
+ transport = next;
25
+ }
26
+ else if (arg === '--port') {
27
+ const next = Number(argv[i + 1]);
28
+ if (Number.isFinite(next))
29
+ port = next;
30
+ }
31
+ }
32
+ return { transport, port };
33
+ }
34
+ /** 从环境构造依赖;LLM / 微信客户端缺失时不抛错,对应的 tool 会返回明确错误。 */
35
+ function buildDeps() {
36
+ const deps = {};
37
+ try {
38
+ const llmConfig = loadLlmConfigFromEnv();
39
+ deps.llm = new LlmClient(llmConfig);
40
+ }
41
+ catch {
42
+ // 缺 LLM 配置:generate_theme 可用 list_themes 兜底
43
+ deps.llm = undefined;
44
+ }
45
+ try {
46
+ const wxConfig = loadConfigFromEnv();
47
+ deps.wechat = new WeChatClient(wxConfig);
48
+ }
49
+ catch {
50
+ // 缺微信凭据:publish_draft 会返回清晰错误
51
+ deps.wechat = undefined;
52
+ }
53
+ return deps;
54
+ }
55
+ // ---------- 本地 Web 编辑器(/editor + /editor/api/*) ----------
56
+ async function readJsonBody(req) {
57
+ let body = '';
58
+ for await (const chunk of req)
59
+ body += chunk;
60
+ if (!body)
61
+ return {};
62
+ try {
63
+ return JSON.parse(body);
64
+ }
65
+ catch {
66
+ return {};
67
+ }
68
+ }
69
+ function sendJson(res, obj, status = 200) {
70
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
71
+ res.end(JSON.stringify(obj));
72
+ }
73
+ function sendErr(res, error, status = 400) {
74
+ sendJson(res, { error }, status);
75
+ }
76
+ async function handleEditorApi(req, res, path, url, deps) {
77
+ try {
78
+ if (path === '/editor/api/themes' && req.method === 'GET') {
79
+ const savedNames = new Set(listSavedThemes().themes.map((t) => t.name));
80
+ const themes = listThemes().themes.map((t) => ({ ...t, origin: savedNames.has(t.name) ? 'saved' : 'preset' }));
81
+ return sendJson(res, { themes });
82
+ }
83
+ if (path === '/editor/api/render' && req.method === 'POST') {
84
+ const b = await readJsonBody(req);
85
+ const markdown = typeof b.markdown === 'string' ? b.markdown : '';
86
+ if (!markdown)
87
+ return sendErr(res, { code: 'missing_content', message: '缺少 markdown 正文。', hint: '请提供 markdown 字段。' });
88
+ const theme = resolveTheme(b.theme ?? 'magazine');
89
+ // 编辑器用轻量渲染:跳过 Chromium 截图(每次键入都截会很贵),仅返回 HTML + 校验
90
+ const r = await renderPreview(markdown, theme, { includeScreenshot: false });
91
+ return sendJson(res, {
92
+ html: r.html,
93
+ validation: r.validation,
94
+ theme: r.theme,
95
+ });
96
+ }
97
+ if (path === '/editor/api/validate' && req.method === 'POST') {
98
+ const b = await readJsonBody(req);
99
+ return sendJson(res, validateArticle(typeof b.html === 'string' ? b.html : ''));
100
+ }
101
+ if (path === '/editor/api/generate' && req.method === 'POST') {
102
+ if (!deps.llm)
103
+ return sendErr(res, { code: 'missing_llm_config', message: '需要 LLM 配置。', hint: '请配置 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL 后重启服务。' });
104
+ const b = await readJsonBody(req);
105
+ const result = await generateTheme({
106
+ prompt: typeof b.prompt === 'string' ? b.prompt : undefined,
107
+ article: typeof b.article === 'string' ? b.article : undefined,
108
+ baseTheme: typeof b.baseTheme === 'string' ? b.baseTheme : undefined,
109
+ }, deps.llm);
110
+ let saved = false;
111
+ if (b.save && result.theme) {
112
+ saveTheme(result.theme);
113
+ saved = true;
114
+ }
115
+ return sendJson(res, {
116
+ theme: result.theme ? { ...result.theme, origin: 'saved' } : undefined,
117
+ fallback: result.fallback,
118
+ repairAttempts: result.repairAttempts,
119
+ saved,
120
+ analysis: result.analysis,
121
+ previewPng: result.previewPng ? result.previewPng.toString('base64') : undefined,
122
+ });
123
+ }
124
+ if (path === '/editor/api/savetheme' && req.method === 'POST') {
125
+ const b = await readJsonBody(req);
126
+ if (!b.theme)
127
+ return sendErr(res, { code: 'invalid_theme', message: '缺少 theme。', hint: '请提供主题 JSON 对象。' });
128
+ const t = saveTheme(b.theme);
129
+ return sendJson(res, { theme: t.theme });
130
+ }
131
+ if (path === '/editor/api/export' && req.method === 'GET') {
132
+ const name = url.searchParams.get('theme') || '';
133
+ const r = exportTheme(name);
134
+ return sendJson(res, { theme: r.theme });
135
+ }
136
+ if (path === '/editor/api/publish' && req.method === 'POST') {
137
+ if (!deps.wechat)
138
+ return sendErr(res, { code: 'missing_wechat_credential', message: '需要微信凭据。', hint: '请配置 WECHAT_APP_ID / WECHAT_APP_SECRET 后重启服务。' });
139
+ const b = await readJsonBody(req);
140
+ const title = typeof b.title === 'string' && b.title.trim() ? b.title.trim() : '';
141
+ const markdown = typeof b.markdown === 'string' ? b.markdown : '';
142
+ if (!title)
143
+ return sendErr(res, { code: 'missing_title', message: '缺少标题。', hint: '请提供 title。' });
144
+ const theme = resolveTheme(typeof b.theme === 'string' ? b.theme : 'magazine');
145
+ const { html } = await renderPreview(markdown, theme);
146
+ // 封面:优先本地上传的原始字节(base64),否则用 URL
147
+ const coverData = typeof b.coverData === 'string' && b.coverData
148
+ ? { bytes: new Uint8Array(Buffer.from(b.coverData, 'base64')), mimeType: typeof b.coverMime === 'string' ? b.coverMime : 'image/jpeg' }
149
+ : undefined;
150
+ const result = await publisherPublishDraft(deps.wechat, {
151
+ content: html,
152
+ title,
153
+ coverImage: typeof b.coverImage === 'string' ? b.coverImage : undefined,
154
+ coverData,
155
+ author: typeof b.author === 'string' ? b.author : undefined,
156
+ relocate: b.relocate !== false,
157
+ });
158
+ return sendJson(res, { media_id: result.media_id, uploadedImages: result.uploadedImages, coverMediaId: result.coverMediaId });
159
+ }
160
+ if (path === '/editor/api/optimize' && req.method === 'POST') {
161
+ if (!deps.llm)
162
+ return sendErr(res, { code: 'missing_llm_config', message: '需要 LLM 配置。', hint: '请配置 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL 后重启服务。' });
163
+ const b = await readJsonBody(req);
164
+ const markdown = typeof b.markdown === 'string' ? b.markdown : '';
165
+ const r = await optimizeArticle(markdown, deps.llm);
166
+ return sendJson(res, { markdown: r.markdown });
167
+ }
168
+ return sendErr(res, { code: 'not_found', message: '未知 /editor/api 端点:' + path, hint: '请检查路径。' }, 404);
169
+ }
170
+ catch (error) {
171
+ const e = asServiceError(error);
172
+ return sendErr(res, e.error);
173
+ }
174
+ }
175
+ async function runStdio() {
176
+ const server = createMcpServer(buildDeps());
177
+ const transport = new StdioServerTransport();
178
+ await server.connect(transport);
179
+ console.error('[stylewx] MCP server (stdio) started.');
180
+ }
181
+ async function runHttp(port) {
182
+ const transports = new Map();
183
+ const deps = buildDeps();
184
+ const editorHtml = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../editor.html'), 'utf8');
185
+ const server = createServer(async (req, res) => {
186
+ const url = new URL(req.url ?? '/', 'http://localhost');
187
+ const path = url.pathname;
188
+ // 本地 Web 编辑器(WeMD 风格)
189
+ if (path === '/editor') {
190
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
191
+ res.end(editorHtml);
192
+ return;
193
+ }
194
+ if (path.startsWith('/editor/api/')) {
195
+ await handleEditorApi(req, res, path, url, deps);
196
+ return;
197
+ }
198
+ if (path !== '/mcp') {
199
+ res.writeHead(404);
200
+ res.end('Not Found');
201
+ return;
202
+ }
203
+ const sessionId = req.headers['mcp-session-id'];
204
+ let transport = sessionId ? transports.get(sessionId) : undefined;
205
+ const isNew = !transport;
206
+ if (req.method === 'GET') {
207
+ // SSE 拉起:必须有既有会话,否则按规范 404
208
+ if (!transport) {
209
+ res.writeHead(404);
210
+ res.end('Not Found');
211
+ return;
212
+ }
213
+ await transport.handleRequest(req, res);
214
+ return;
215
+ }
216
+ if (!transport) {
217
+ transport = new StreamableHTTPServerTransport({
218
+ sessionIdGenerator: () => randomUUID(),
219
+ });
220
+ const mcp = createMcpServer(buildDeps());
221
+ transport.onclose = () => {
222
+ if (transport && transport.sessionId)
223
+ transports.delete(transport.sessionId);
224
+ };
225
+ await mcp.connect(transport);
226
+ }
227
+ await transport.handleRequest(req, res);
228
+ // 会话 ID 在处理 initialize 之后才会生成,这里再登记到会话表
229
+ if (isNew && transport.sessionId) {
230
+ transports.set(transport.sessionId, transport);
231
+ }
232
+ });
233
+ server.listen(port, () => {
234
+ // eslint-disable-next-line no-console
235
+ console.error(`[stylewx] MCP server (Streamable HTTP) listening on http://localhost:${port}/mcp`);
236
+ });
237
+ const shutdown = () => {
238
+ server.close();
239
+ };
240
+ process.on('SIGINT', shutdown);
241
+ process.on('SIGTERM', shutdown);
242
+ }
243
+ async function main() {
244
+ const options = parseArgs(process.argv.slice(2));
245
+ if (options.transport === 'http') {
246
+ await runHttp(options.port);
247
+ }
248
+ else {
249
+ await runStdio();
250
+ }
251
+ }
252
+ main().catch((error) => {
253
+ // eslint-disable-next-line no-console
254
+ console.error('[stylewx] fatal:', error instanceof Error ? error.message : error);
255
+ process.exit(1);
256
+ });
257
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mcp.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.test.d.ts","sourceRoot":"","sources":["../src/mcp.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Client } from '@modelcontextprotocol/sdk/client';
3
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory';
4
+ import { createMcpServer } from './server.js';
5
+ import { getPresetTheme } from '@stylewx/theme';
6
+ import { WeChatClient } from '@stylewx/publisher';
7
+ async function startClient(deps = {}) {
8
+ const server = createMcpServer(deps);
9
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
10
+ await server.connect(serverTransport);
11
+ const client = new Client({ name: 'test', version: '0.0.0' });
12
+ await client.connect(clientTransport);
13
+ return { client, server };
14
+ }
15
+ function parseText(result) {
16
+ const text = result.content.find((c) => c.type === 'text')?.text ?? '';
17
+ return JSON.parse(text);
18
+ }
19
+ function fakeLlm(responses) {
20
+ let i = 0;
21
+ const completeJson = async (_m, _o) => {
22
+ const r = responses[i];
23
+ i += 1;
24
+ if (r instanceof Error)
25
+ throw r;
26
+ return r;
27
+ };
28
+ return { completeJson };
29
+ }
30
+ describe('stylewx MCP Server (in-memory)', () => {
31
+ it('list_themes 返回 ≥6 套主题', async () => {
32
+ const { client } = await startClient();
33
+ const names = await client.listTools();
34
+ expect(names.tools.some((t) => t.name === 'list_themes')).toBe(true);
35
+ const res = await client.callTool({ name: 'list_themes', arguments: {} });
36
+ const data = parseText(res);
37
+ expect(data.themes.length).toBeGreaterThanOrEqual(6);
38
+ });
39
+ it('list 出全部 9 个 tools', async () => {
40
+ const { client } = await startClient();
41
+ const { tools } = await client.listTools();
42
+ const names = tools.map((t) => t.name).sort();
43
+ expect(names).toEqual([
44
+ 'analyze_article',
45
+ 'export_theme',
46
+ 'generate_theme',
47
+ 'list_saved_themes',
48
+ 'list_themes',
49
+ 'publish_draft',
50
+ 'render_preview',
51
+ 'save_theme',
52
+ 'validate_article',
53
+ ]);
54
+ });
55
+ it('analyze_article 返回分析结论', async () => {
56
+ const { client } = await startClient();
57
+ const res = await client.callTool({ name: 'analyze_article', arguments: { markdown: '这是一篇讲前端框架与性能优化的技术文章。' } });
58
+ const data = parseText(res);
59
+ expect(data.content.type).toBe('tech');
60
+ expect(data.suggestedTheme.name).toBe('tech-minimal');
61
+ });
62
+ it('render_preview 渲染出无 <style> 的 HTML', async () => {
63
+ const theme = getPresetTheme('tech-minimal');
64
+ const { client } = await startClient();
65
+ const res = await client.callTool({
66
+ name: 'render_preview',
67
+ arguments: { markdown: '# 标题\n\n正文。\n\n- 项一\n', theme: JSON.parse(JSON.stringify(theme)) },
68
+ });
69
+ const data = parseText(res);
70
+ expect(data.html).not.toContain('<style');
71
+ expect(data.html).not.toContain('class=');
72
+ expect(data.validation.pass).toBe(true);
73
+ });
74
+ it('validate_article 返回结构化报告', async () => {
75
+ const { client } = await startClient();
76
+ const res = await client.callTool({ name: 'validate_article', arguments: { html: '<section><script>x</script><p>ok</p></section>' } });
77
+ const data = parseText(res);
78
+ expect(data.report.pass).toBe(false);
79
+ expect(data.report.issues.some((i) => i.rule === 'no-forbidden-tag')).toBe(true);
80
+ });
81
+ it('generate_theme 缺 LLM 时返回统一错误(isError,不崩溃)', async () => {
82
+ const { client } = await startClient();
83
+ const res = await client.callTool({ name: 'generate_theme', arguments: { prompt: '科技风' } });
84
+ expect(res.isError).toBe(true);
85
+ const data = parseText(res);
86
+ expect(data.error.code).toBe('missing_llm_config');
87
+ expect(data.error.hint).toBeTruthy();
88
+ });
89
+ it('generate_theme 提供 LLM 时生成主题', async () => {
90
+ const { client } = await startClient({ llm: fakeLlm([getPresetTheme('business')]) });
91
+ const res = await client.callTool({ name: 'generate_theme', arguments: { prompt: '商务风' } });
92
+ const data = parseText(res);
93
+ expect(data.fallback).toBe(false);
94
+ expect(data.theme.name).toBe('business');
95
+ });
96
+ it('publish_draft 缺微信凭据时返回清晰错误', async () => {
97
+ const { client } = await startClient();
98
+ const res = await client.callTool({
99
+ name: 'publish_draft',
100
+ arguments: { title: '标题', content: '<p></p>', contentHtml: '<p>x</p>' },
101
+ });
102
+ expect(res.isError).toBe(true);
103
+ const data = parseText(res);
104
+ expect(data.error.code).toBe('missing_wechat_credential');
105
+ });
106
+ it('publish_draft 提供微信客户端时发布成功', async () => {
107
+ const fetchMock = async (input) => {
108
+ const url = String(input);
109
+ if (url.includes('/cgi-bin/token'))
110
+ return Response.json({ access_token: 't', expires_in: 7200 });
111
+ if (url.includes('/cgi-bin/material/add_material'))
112
+ return Response.json({ media_id: 'm', url: 'https://mmbiz.qpic.cn/u' });
113
+ if (url.includes('/cgi-bin/draft/add'))
114
+ return Response.json({ media_id: 'draft_mcp' }, { status: 200 });
115
+ return Response.json({});
116
+ };
117
+ const wechat = new WeChatClient({ appId: 'a', appSecret: 's', baseUrl: 'https://api.weixin.qq.com', fetchImpl: fetchMock });
118
+ const { client } = await startClient({ wechat });
119
+ const res = await client.callTool({
120
+ name: 'publish_draft',
121
+ arguments: {
122
+ title: '标题', html: '<section><p>hello</p></section>',
123
+ coverImage: 'https://mmbiz.qpic.cn/cover.png',
124
+ },
125
+ });
126
+ const data = parseText(res);
127
+ expect(data.media_id).toBe('draft_mcp');
128
+ });
129
+ });
130
+ //# sourceMappingURL=mcp.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.test.js","sourceRoot":"","sources":["../src/mcp.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ToolDeps } from './tools.js';
3
+ export declare const SERVER_NAME = "stylewx";
4
+ export declare const SERVER_VERSION = "0.1.0";
5
+ /** 创建已注册全部 6 个 tools 的 MCP Server。 */
6
+ export declare function createMcpServer(deps?: ToolDeps): McpServer;
7
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAEnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,eAAO,MAAM,WAAW,YAAY,CAAA;AACpC,eAAO,MAAM,cAAc,UAAU,CAAA;AAErC,sCAAsC;AACtC,wBAAgB,eAAe,CAAC,IAAI,GAAE,QAAa,GAAG,SAAS,CAO9D"}
package/dist/server.js ADDED
@@ -0,0 +1,14 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerMcpTools } from './tools.js';
3
+ export const SERVER_NAME = 'stylewx';
4
+ export const SERVER_VERSION = '0.1.0';
5
+ /** 创建已注册全部 6 个 tools 的 MCP Server。 */
6
+ export function createMcpServer(deps = {}) {
7
+ const server = new McpServer({
8
+ name: SERVER_NAME,
9
+ version: SERVER_VERSION,
10
+ });
11
+ registerMcpTools(server, deps);
12
+ return server;
13
+ }
14
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAG7C,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAA;AACpC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAA;AAErC,sCAAsC;AACtC,MAAM,UAAU,eAAe,CAAC,OAAiB,EAAE;IACjD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,cAAc;KACxB,CAAC,CAAA;IACF,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAC9B,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * MCP tools 定义与处理逻辑。
3
+ * 每个 tool 的 description 与每个参数的描述都面向「调用它的 Agent」撰写:
4
+ * 说明使用场景、参数含义、典型错误及处理。
5
+ */
6
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ import type { LlmClient } from '@stylewx/service';
8
+ import type { WeChatClient } from '@stylewx/publisher';
9
+ export interface ToolDeps {
10
+ llm?: LlmClient;
11
+ wechat?: WeChatClient;
12
+ }
13
+ export declare function registerMcpTools(server: McpServer, deps: ToolDeps): void;
14
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAkBxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,MAAM,WAAW,QAAQ;IACvB,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB;AAyCD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CA2PxE"}
package/dist/tools.js ADDED
@@ -0,0 +1,227 @@
1
+ import { z } from 'zod';
2
+ import { analyzeArticle, generateTheme, listThemes, listSavedThemes, saveTheme, exportTheme, deleteTheme, publishDraft, renderPreview, resolveTheme, validateArticle, serviceError, asServiceError, } from '@stylewx/service';
3
+ function errorResult(code, message, hint) {
4
+ return {
5
+ isError: true,
6
+ content: [{ type: 'text', text: JSON.stringify(serviceError(code, message, hint)) }],
7
+ };
8
+ }
9
+ function errorResultFrom(error) {
10
+ if (error && typeof error === 'object' && 'error' in error) {
11
+ const e = error.error;
12
+ if (e && typeof e.code === 'string')
13
+ return errorResult(e.code, e.message, e.hint);
14
+ }
15
+ const e = asServiceError(error);
16
+ return errorResult(e.error.code, e.error.message, e.error.hint);
17
+ }
18
+ function textResult(text) {
19
+ return { content: [{ type: 'text', text: typeof text === 'string' ? text : JSON.stringify(text) }] };
20
+ }
21
+ function jsonText(value) {
22
+ return JSON.stringify(value);
23
+ }
24
+ /** 兜底包装:把 handler 抛出的任何错误转成统一错误结果,并透传参数。 */
25
+ function wrap(fn) {
26
+ return async (args) => {
27
+ try {
28
+ return await fn(args);
29
+ }
30
+ catch (error) {
31
+ return errorResultFrom(error);
32
+ }
33
+ };
34
+ }
35
+ const themeObjSchema = z.record(z.string(), z.unknown());
36
+ export function registerMcpTools(server, deps) {
37
+ // ---- list_themes ----
38
+ server.registerTool('list_themes', {
39
+ title: '列出预置主题',
40
+ description: '列出全部预置排版主题(含名称、描述、tokens 摘要)。主题描述说明了每套风格的适用场景,可直接用于选择。' +
41
+ '调用后请阅读 description,结合文章内容挑选最契合的主题;需要自定义时再用 generate_theme。',
42
+ inputSchema: {},
43
+ }, wrap(async () => {
44
+ return textResult(listThemes());
45
+ }));
46
+ // ---- list_saved_themes ----
47
+ server.registerTool('list_saved_themes', {
48
+ title: '列出已保存主题',
49
+ description: '列出你本地已保存的自定义 / AI 生成主题(持久化在 ~/.stylewx/themes.json)。' +
50
+ '这些主题来自 generate_theme(save=true) 或 save_theme,可直接按名称传给 render_preview / publish_draft 复用。',
51
+ inputSchema: {},
52
+ }, wrap(async () => textResult(listSavedThemes())));
53
+ // ---- save_theme ----
54
+ server.registerTool('save_theme', {
55
+ title: '保存主题',
56
+ description: '把一个主题对象保存到本地主题库(~/.stylewx/themes.json),方便以后复用。同名会覆盖。' +
57
+ '主题会先过 Schema + 微信白名单校验,非法会返回明确错误。通常把 generate_theme 返回的 theme 直接存进来。',
58
+ inputSchema: {
59
+ theme: themeObjSchema.describe('要保存的完整主题对象(含 name/description/tokens/blocks),来自 generate_theme 的返回。'),
60
+ name: z.string().optional().describe('可选:覆盖主题名;缺省用 theme.name。'),
61
+ },
62
+ }, wrap(async ({ theme, name }) => {
63
+ const t = name && typeof theme === 'object' ? { ...theme, name } : theme;
64
+ return textResult(saveTheme(t));
65
+ }));
66
+ // ---- export_theme ----
67
+ server.registerTool('export_theme', {
68
+ title: '导出主题',
69
+ description: '把一个主题导出为完整 JSON(含 name/description/tokens/blocks),可直接传给 render_preview / publish_draft 复用,' +
70
+ '或保存下来分享。支持:已保存主题名、预置主题名、或完整主题对象。',
71
+ inputSchema: {
72
+ theme: z.union([z.string(), themeObjSchema]).describe('主题名(已保存或预置)或完整主题对象。'),
73
+ },
74
+ }, wrap(async ({ theme }) => textResult(exportTheme(theme))));
75
+ // ---- analyze_article ----
76
+ server.registerTool('analyze_article', {
77
+ title: '分析文章',
78
+ description: '分析一篇 Markdown 文章:推断内容类型、情绪基调、建议主题方向、字数与预计阅读时长。' +
79
+ '在正式开始排版前先调用它,能帮你选定方向;generate_theme 与 render_preview 会参考这里的结论。',
80
+ inputSchema: {
81
+ markdown: z
82
+ .string()
83
+ .describe('文章正文(Markdown,无需渲染)。内容越长,分析越准确。'),
84
+ },
85
+ }, wrap(async ({ markdown }) => textResult(analyzeArticle(markdown))));
86
+ // ---- generate_theme ----
87
+ server.registerTool('generate_theme', {
88
+ title: '生成主题',
89
+ description: '通过 LLM 生成一版符合 Schema 的排版主题 JSON。可传入风格描述 prompt,或整篇 article(此时会先分析内容再生成)。' +
90
+ '内置自检修复循环:生成结果会经过 zod 与微信白名单校验,失败会自动回传错误要求模型修复(最多重试 2 次),仍失败则降级返回最接近的预置主题并在 fallback=true 标记。' +
91
+ '成功后会自动渲染一篇内置示例文章并附预览截图。需要配置 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL 环境变量。',
92
+ inputSchema: {
93
+ prompt: z
94
+ .string()
95
+ .optional()
96
+ .describe('风格/设计需求描述,例如「科技感、深色底、青绿色点缀」。可与 article 二选一或同时提供。'),
97
+ article: z
98
+ .string()
99
+ .optional()
100
+ .describe('文章全文(Markdown)。提供时会先做一次内容分析,再据此生成主题;分析结论会随结果返回。'),
101
+ baseTheme: z
102
+ .string()
103
+ .optional()
104
+ .describe('预置主题名(可选)。以此为基础微调,例如 tech-minimal / business / magazine / gov-red / academic / dark-code。'),
105
+ save: z
106
+ .boolean()
107
+ .optional()
108
+ .describe('是否把生成的主题保存到本地主题库(~/.stylewx/themes.json),便于以后按名复用。默认 false。'),
109
+ },
110
+ }, async (args) => {
111
+ try {
112
+ if (!deps.llm) {
113
+ return errorResult('missing_llm_config', 'generate_theme 需要 LLM 配置,但当前未提供 LLM 客户端。', '请配置环境变量 LLM_BASE_URL、LLM_API_KEY、LLM_MODEL 后重启服务;或先用 list_themes 选择一个预置主题。');
114
+ }
115
+ const result = await generateTheme({ prompt: args.prompt, article: args.article, baseTheme: args.baseTheme }, deps.llm);
116
+ let saved = false;
117
+ if (args.save && result.theme) {
118
+ saveTheme(result.theme);
119
+ saved = true;
120
+ }
121
+ const payload = {
122
+ theme: result.theme,
123
+ fallback: result.fallback,
124
+ repairAttempts: result.repairAttempts,
125
+ errorDetail: result.errorDetail,
126
+ analysis: result.analysis,
127
+ saved,
128
+ savedLocation: saved ? '~/.stylewx/themes.json' : undefined,
129
+ };
130
+ const content = [{ type: 'text', text: jsonText(payload) }];
131
+ if (result.previewPng) {
132
+ content.push({ type: 'image', data: result.previewPng.toString('base64'), mimeType: 'image/png' });
133
+ }
134
+ return { content };
135
+ }
136
+ catch (error) {
137
+ return errorResultFrom(error);
138
+ }
139
+ });
140
+ // ---- render_preview ----
141
+ server.registerTool('render_preview', {
142
+ title: '渲染预览',
143
+ description: '把 Markdown + 主题渲染成微信兼容的内联样式 HTML,并返回 iPhone 视口(390px 宽)的预览截图(PNG)与校验报告。' +
144
+ '这是排版的核心链路:输出 HTML 不含 <style>/<link>/class 依赖,全部样式已内联。' +
145
+ '若校验报告 pass=false,请先按 issues 中的建议修正后再发布。',
146
+ inputSchema: {
147
+ markdown: z.string().describe('文章正文(Markdown)。'),
148
+ theme: z
149
+ .union([z.string(), themeObjSchema])
150
+ .describe('主题:可为预置主题名(如 tech-minimal),或完整主题 JSON 对象(必须含 name/description/tokens/blocks 且 CSS 属性在微信白名单内)。可用 generate_theme 生成,或从 list_themes 的预置主题中选取。'),
151
+ },
152
+ }, wrap(async ({ markdown, theme }) => {
153
+ const resolved = resolveTheme(theme);
154
+ const result = await renderPreview(markdown, resolved);
155
+ const content = [
156
+ { type: 'text', text: jsonText({ html: result.html, validation: result.validation, theme: result.theme }) },
157
+ ];
158
+ if (result.screenshotPng) {
159
+ content.push({ type: 'image', data: result.screenshotPng.toString('base64'), mimeType: 'image/png' });
160
+ }
161
+ return { content };
162
+ }));
163
+ // ---- validate_article ----
164
+ server.registerTool('validate_article', {
165
+ title: '校验文章',
166
+ description: '校验一段 HTML 是否可发布到微信公众号。检查:微信白名单外的 CSS 属性、被过滤的标签(script/style/iframe 等)、' +
167
+ '事件属性(on*)、javascript: 链接、外链图片、图片数量与正文长度。返回结构化报告 { pass, issues: [{ rule, severity, message, suggestion, location }] },' +
168
+ 'suggestion 为可直接执行的修复建议。建议 publish 前必调。',
169
+ inputSchema: {
170
+ html: z.string().describe('待校验的 HTML(通常为 render_preview 的输出)。'),
171
+ },
172
+ }, wrap(async ({ html }) => textResult(validateArticle(html))));
173
+ // ---- publish_draft ----
174
+ server.registerTool('publish_draft', {
175
+ title: '发布草稿',
176
+ description: '把文章发布到微信公众号「草稿箱」(draft/add)。支持:把外链图片自动搬运到微信素材库并替换为 mmbiz.qpic.cn 链接、' +
177
+ '上传封面图得到 thumb_media_id、生成摘要。发布完成后仍需人工在公众号后台确认,本工具不会自动群发。' +
178
+ '需要配置 WECHAT_APP_ID / WECHAT_APP_SECRET 环境变量。缺凭据时不会静默失败,会返回明确错误。',
179
+ inputSchema: {
180
+ markdown: z.string().optional().describe('文章正文(Markdown)。与 html 二选一;若提供则先用传入的 theme 渲染成 HTML。'),
181
+ html: z.string().optional().describe('已渲染的内联样式 HTML。与 markdown 二选一。'),
182
+ theme: z
183
+ .union([z.string(), themeObjSchema])
184
+ .optional()
185
+ .describe('对 markdown 做渲染时使用的主题(预置主题名或主题 JSON;html 已提供时可省略)。'),
186
+ title: z.string().describe('文章标题(必填)。'),
187
+ author: z.string().optional().describe('作者名(可选)。'),
188
+ digest: z.string().optional().describe('摘要(可选)。缺省会自动从正文截取前 120 字。'),
189
+ coverImage: z.string().optional().describe('封面图 URL(建议 900×383 / 2.35:1)。缺省会用正文第一张图作为封面;若正文无图则必须提供。'),
190
+ contentSourceUrl: z.string().optional().describe('原文链接(可选)。'),
191
+ needOpenComment: z.boolean().optional().describe('是否允许用户评论,默认按公众号设置。'),
192
+ },
193
+ }, async (args) => {
194
+ try {
195
+ if (!deps.wechat) {
196
+ return errorResult('missing_wechat_credential', 'publish_draft 需要微信公众号凭据,但当前未配置微信客户端。', '请配置环境变量 WECHAT_APP_ID 与 WECHAT_APP_SECRET 后重启服务。');
197
+ }
198
+ let content;
199
+ if (args.html) {
200
+ content = args.html;
201
+ }
202
+ else if (args.markdown) {
203
+ if (!args.theme)
204
+ throw serviceError('invalid_theme', '缺少 theme:使用 markdown 发布时需提供主题。', '请提供 theme 对象,或改用 html 参数。');
205
+ const preview = await renderPreview(args.markdown, resolveTheme(args.theme));
206
+ content = preview.html;
207
+ }
208
+ else {
209
+ throw serviceError('missing_content', '缺少正文内容。', '请提供 markdown 或 html 至少其一。');
210
+ }
211
+ const result = await publishDraft(deps.wechat, {
212
+ content,
213
+ title: args.title,
214
+ author: args.author,
215
+ digest: args.digest,
216
+ coverImage: args.coverImage,
217
+ contentSourceUrl: args.contentSourceUrl,
218
+ needOpenComment: args.needOpenComment,
219
+ });
220
+ return textResult(result);
221
+ }
222
+ catch (error) {
223
+ return errorResultFrom(error);
224
+ }
225
+ });
226
+ }
227
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/editor.html ADDED
@@ -0,0 +1,417 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>stylewx 编辑器</title>
7
+ <style>
8
+ :root{
9
+ --bg:#f3f4f7; --panel:#fff; --line:#e8eaee; --line2:#f0f2f5;
10
+ --ink:#23272e; --ink2:#4b5058; --muted:#98a0ab; --faint:#c4cbd4;
11
+ --brand:#2563eb; --brand-d:#1d4ed8; --brand-sub:#e9f0ff;
12
+ --ok:#12a150; --err:#e5484d; --warn:#b4690e;
13
+ --sidebar-w:236px; --radius:14px; --radius-sm:10px;
14
+ --shadow:0 8px 30px rgba(20,30,50,.06), 0 1px 2px rgba(20,30,50,.04);
15
+ --shadow-lg:0 16px 46px rgba(20,30,50,.10);
16
+ --mono:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;
17
+ }
18
+ *{box-sizing:border-box}
19
+ html,body{margin:0;height:100%}
20
+ body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--ink);overflow:hidden}
21
+ button{font:inherit}
22
+ .app{display:flex;gap:14px;padding:14px;height:100vh}
23
+
24
+ /* ---------- 左侧栏 ---------- */
25
+ .sidebar{width:var(--sidebar-w);flex:none;background:var(--panel);border-radius:var(--radius);box-shadow:var(--shadow);display:flex;flex-direction:column;overflow:hidden}
26
+ .side-top{display:flex;align-items:center;gap:9px;padding:16px 16px 14px}
27
+ .logo{width:28px;height:28px;border-radius:9px;background:var(--brand);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:15px;box-shadow:0 4px 10px rgba(37,99,235,.3)}
28
+ .side-brand{font-weight:700;font-size:14px}
29
+ .side-brand small{color:var(--muted);font-weight:400;margin-left:6px;font-size:11px}
30
+ .new-btn{margin:4px 14px 16px;display:flex;align-items:center;justify-content:center;gap:7px;background:var(--brand);color:#fff;border:0;border-radius:var(--radius-sm);padding:11px;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 6px 16px rgba(37,99,235,.28);transition:.15s}
31
+ .new-btn:hover{background:var(--brand-d);transform:translateY(-1px)}
32
+ .subnav{padding:0 12px 12px;display:flex;flex-direction:column;gap:3px}
33
+ .subnav button{display:flex;align-items:center;gap:9px;width:100%;text-align:left;background:none;border:0;border-radius:9px;padding:9px 10px;font-size:13px;color:var(--ink2);cursor:pointer;transition:.12s}
34
+ .subnav button:hover{background:#f2f4f7}
35
+ .subnav button.active{background:var(--brand-sub);color:var(--brand);font-weight:600}
36
+ .side-label{padding:10px 18px 6px;font-size:11px;color:var(--muted);font-weight:600;letter-spacing:.03em}
37
+ .history{flex:1;overflow:auto;padding:0 12px 14px}
38
+ .hist-search{width:100%;border:1px solid var(--line);border-radius:var(--radius-sm);padding:8px 11px;font-size:12px;margin-bottom:8px;background:#fbfcfe;color:var(--ink)}
39
+ .hist-search::placeholder{color:var(--faint)}
40
+ .hist-item{padding:10px 11px;border-radius:9px;cursor:pointer;border:1px solid transparent}
41
+ .hist-item:hover{background:#f2f4f7}
42
+ .hist-item .h-title{font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
43
+ .hist-item .h-meta{font-size:11px;color:var(--muted);margin-top:2px}
44
+ .side-foot{padding:12px 18px;border-top:1px solid var(--line2);font-size:11px;color:var(--muted)}
45
+
46
+ /* ---------- 主区 ---------- */
47
+ .main{flex:1;display:flex;flex-direction:column;gap:14px;min-width:0}
48
+ .topbar{display:flex;align-items:center;gap:8px;padding:11px 16px;background:var(--panel);border-radius:var(--radius);box-shadow:var(--shadow);flex-wrap:wrap}
49
+ .btn{border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:var(--radius-sm);padding:7px 12px;font-size:12.5px;cursor:pointer;transition:.12s;white-space:nowrap;box-shadow:0 1px 2px rgba(20,30,50,.03);display:inline-flex;align-items:center;gap:6px}
50
+ .btn svg,.subnav button svg{flex:none}
51
+ .btn:hover{background:#f4f6f9;border-color:#dde1e7}
52
+ .btn.primary{background:var(--brand);border-color:var(--brand);color:#fff;box-shadow:0 4px 12px rgba(37,99,235,.26)}
53
+ .btn.primary:hover{background:var(--brand-d);border-color:var(--brand-d)}
54
+ .btn.ghost{border:none;background:none;color:var(--muted);box-shadow:none}
55
+ .btn.ghost:hover{background:#f2f4f7;color:var(--ink)}
56
+ .grow{flex:1}
57
+ .topbar .lbl{font-size:12px;color:var(--muted)}
58
+ select,input[type=text]{border:1px solid var(--line);border-radius:var(--radius-sm);padding:7px 10px;font-size:12.5px;background:#fff;color:var(--ink);outline:none;box-shadow:0 1px 2px rgba(20,30,50,.03)}
59
+ select:focus,input:focus{border-color:var(--brand);box-shadow:0 0 0 3px rgba(37,99,235,.12)}
60
+ input#title{width:170px}
61
+ details.theme-panel{position:relative}
62
+ details.theme-panel summary{cursor:pointer;padding:7px 12px;font-size:12.5px;border:1px solid var(--line);border-radius:var(--radius-sm);color:var(--muted);box-shadow:0 1px 2px rgba(20,30,50,.03)}
63
+ .theme-box{position:absolute;right:0;top:calc(100% + 8px);background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow-lg);padding:12px;width:260px;z-index:30}
64
+ .theme-box h4{margin:0 0 8px;font-size:12px;color:var(--muted);font-weight:600}
65
+
66
+ /* ---------- 内容区 ---------- */
67
+ .content{flex:1;display:flex;gap:14px;min-height:0}
68
+ .editor-pane{width:50%;min-width:0;display:flex;flex-direction:column;border-radius:var(--radius);background:var(--panel);box-shadow:var(--shadow);overflow:hidden}
69
+ .preview-pane{width:50%;min-width:0;display:flex;flex-direction:column;border-radius:var(--radius);background:var(--panel);box-shadow:var(--shadow);overflow:hidden}
70
+ .pane-toolbar{display:flex;align-items:center;gap:2px;padding:9px 12px;border-bottom:1px solid var(--line2);background:#fff;flex-wrap:wrap}
71
+ .tb-btn{width:29px;height:29px;border:0;border-radius:8px;background:none;color:var(--ink2);font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.12s}
72
+ .tb-btn:hover{background:#eef1f5;color:var(--ink)}
73
+ .tb-btn b{font-weight:700}
74
+ .tb-btn svg{display:block;margin:auto}
75
+ /* Typora 式表格大小选择器 */
76
+ .table-picker{position:fixed;z-index:70;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow-lg);padding:10px 11px;display:none}
77
+ .table-picker .tlabel{font-size:12px;color:var(--ink2);margin-bottom:9px;text-align:center;font-weight:600}
78
+ .table-picker .tgrid{display:grid;grid-template-columns:repeat(10,16px);grid-auto-rows:16px;gap:3px}
79
+ .table-picker .tcell{background:#eef1f5;border-radius:3px;cursor:pointer}
80
+ .table-picker .tcell.on{background:var(--brand)}
81
+ .table-picker .tcell:hover{background:var(--brand);opacity:.75}
82
+ .tb-sep{width:1px;height:16px;background:var(--line);margin:0 5px}
83
+ .markdown{flex:1;width:100%;border:0;resize:none;padding:22px 26px;font:14px/1.8 var(--mono);background:#fff;color:var(--ink);outline:none}
84
+ .markdown::placeholder{color:var(--faint)}
85
+ .ed-status{display:flex;gap:18px;padding:9px 20px;border-top:1px solid var(--line2);font-size:12px;color:var(--muted);background:#fff}
86
+ .preview-head{display:flex;align-items:center;gap:8px;padding:10px 16px;border-bottom:1px solid var(--line2);font-size:12.5px;color:var(--ink2);background:#fff}
87
+ .preview-head .tab{padding:5px 13px;border-radius:8px;cursor:pointer}
88
+ .preview-head .tab.active{background:var(--brand-sub);color:var(--brand);font-weight:600}
89
+ .preview-wrap{flex:1;overflow:auto;background:#eef1f5;padding:20px;display:flex;justify-content:center}
90
+ iframe.preview{width:390px;min-height:100%;border:0;background:#fff;border-radius:16px;box-shadow:0 10px 34px rgba(20,30,50,.12);align-self:flex-start}
91
+ .val-bar{padding:9px 16px;border-top:1px solid var(--line2);font-size:12px;color:var(--muted);background:#fbfcfe;white-space:pre-wrap;max-height:110px;overflow:auto}
92
+ .val-bar .ok{color:var(--ok)} .val-bar .err{color:var(--err)} .val-bar .warn{color:var(--warn)}
93
+ .toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2329;color:#fff;padding:10px 18px;border-radius:8px;font-size:13px;opacity:0;transition:.25s;pointer-events:none;z-index:60;box-shadow:0 6px 24px rgba(0,0,0,.2)}
94
+ .toast.show{opacity:1}
95
+ .empty{color:#b9c0c9;padding:30px;text-align:center;font-size:12px}
96
+ /* 设置弹窗 */
97
+ .overlay{position:fixed;inset:0;background:rgba(20,24,31,.4);display:none;align-items:center;justify-content:center;z-index:80}
98
+ .overlay.show{display:flex}
99
+ .modal{background:#fff;border-radius:14px;width:420px;max-width:92vw;box-shadow:0 20px 60px rgba(0,0,0,.22);overflow:hidden}
100
+ .modal-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--line)}
101
+ .modal-head b{font-size:14px}
102
+ .modal-close{border:0;background:none;font-size:18px;color:var(--muted);cursor:pointer}
103
+ .modal-body{padding:16px 18px;max-height:60vh;overflow:auto}
104
+ .set-row{display:flex;align-items:center;gap:10px;padding:12px 0;border-bottom:1px solid var(--line2)}
105
+ .set-row:last-child{border-bottom:0}
106
+ .set-row .s-txt{flex:1}
107
+ .set-row .s-txt b{font-size:13px;display:block}
108
+ .set-row .s-txt small{color:var(--muted);font-size:12px;line-height:1.5;display:block;margin-top:2px}
109
+ .switch{position:relative;width:40px;height:22px;flex:none}
110
+ .switch input{opacity:0;width:0;height:0}
111
+ .slider{position:absolute;inset:0;background:#cdd3db;border-radius:22px;transition:.2s}
112
+ .slider:before{content:'';position:absolute;width:16px;height:16px;background:#fff;border-radius:50%;top:3px;left:3px;transition:.2s}
113
+ .switch input:checked + .slider{background:var(--brand)}
114
+ .switch input:checked + .slider:before{transform:translateX(18px)}
115
+ .modal-foot{display:flex;justify-content:flex-end;gap:8px;padding:12px 18px;border-top:1px solid var(--line)}
116
+ </style>
117
+ </head>
118
+ <body>
119
+ <div class="app">
120
+ <!-- 左侧栏 -->
121
+ <aside class="sidebar">
122
+ <div class="side-top">
123
+ <div class="logo">M</div>
124
+ <div class="side-brand">stylewx<small>排版</small></div>
125
+ </div>
126
+ <div class="subnav">
127
+ <button class="active" id="navTheme"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a9 9 0 1 0 0 18c1.5 0 2-.8 2-1.8 0-.9-.7-1.3-.7-2.1 0-.8.7-1.4 1.6-1.4H17a4 4 0 0 0 4-4c0-4.4-4-8.7-9-8.7z"/><circle cx="7.5" cy="11" r="1.1" fill="currentColor"/><circle cx="10.5" cy="7.5" r="1.1" fill="currentColor"/><circle cx="14.5" cy="7.5" r="1.1" fill="currentColor"/><circle cx="17" cy="10.5" r="1.1" fill="currentColor"/></svg>文章主题</button>
128
+ <button id="navImgHost"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 19a4 4 0 0 1-.6-7.9 6 6 0 0 1 11.4-1.5A4.5 4.5 0 0 1 18 19z"/><path d="M12 12v6M9 15l3 3 3-3"/></svg>图床设置</button>
129
+ <button id="navAi"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6z"/><path d="M19 14l.8 2.2L22 17l-2.2.8L19 20l-.8-2.2L16 17l2.2-.8z"/><path d="M5 15l.7 1.8 1.8.7-1.8.7L5 20l-.7-1.8-1.8-.7 1.8-.7z"/></svg>AI 优化</button>
130
+ </div>
131
+ <button class="new-btn" id="newBtn">+ 新建文章</button>
132
+ <div class="side-label">历史记录</div>
133
+ <div class="history">
134
+ <input class="hist-search" id="search" placeholder="搜索…" />
135
+ <div id="histList"></div>
136
+ </div>
137
+ <div class="side-foot">stylewx · v0.1.0 · 本地优先</div>
138
+ </aside>
139
+
140
+ <!-- 主区 -->
141
+ <div class="main">
142
+ <div class="topbar">
143
+ <button class="btn ghost" id="undoBtn" title="撤销">↶</button>
144
+ <button class="btn ghost" id="redoBtn" title="重做">↷</button>
145
+ <span class="lbl">主题</span>
146
+ <select id="themeSel" style="max-width:200px"></select>
147
+ <details class="theme-panel" id="themePanel"><summary>主题库 ▾</summary><div class="theme-box"><h4>已保存的自定义主题</h4><div id="themeList" style="max-height:180px;overflow:auto;font-size:12px;color:var(--ink2);line-height:1.9"></div></div></details>
148
+ <button class="btn" id="genBtn" title="按当前文章用 AI 生成主题"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6z"/><path d="M19 14l.8 2.2L22 17l-2.2.8L19 20l-.8-2.2L16 17l2.2-.8z"/><path d="M5 15l.7 1.8 1.8.7-1.8.7L5 20l-.7-1.8-1.8-.7 1.8-.7z"/></svg>生成主题</button>
149
+ <button class="btn" id="saveBtn"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8M7 3v5h8"/></svg>保存主题</button>
150
+ <button class="btn" id="valBtn"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m8 12 3 3 5-6"/></svg>校验</button>
151
+ <span class="grow"></span>
152
+ <label class="lbl">标题</label><input type="text" id="title" placeholder="文章标题" />
153
+ <label class="lbl">封面</label><input type="text" id="cover" placeholder="封面URL(选填)" style="width:150px" title="封面图 URL(900×383/2.35:1);留空则用正文首图或自动生成" />
154
+ <button class="btn" id="coverUpload" title="上传本地图片作封面">上传</button>
155
+ <span id="coverThumbWrp" style="display:none;position:relative;line-height:0">
156
+ <img id="coverThumb" alt="封面预览" style="width:54px;height:26px;object-fit:cover;border-radius:6px;vertical-align:middle;border:1px solid var(--line)" />
157
+ <button class="btn ghost" id="coverClear" title="清除封面" style="position:absolute;top:-9px;right:-9px;padding:0 5px;font-size:13px;line-height:18px;border-radius:9px">×</button>
158
+ </span>
159
+ <input type="file" id="coverFile" accept="image/*" style="display:none" />
160
+ <button class="btn" id="copyHtmlBtn"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="3" width="8" height="4" rx="1"/><path d="M8 5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/></svg>复制 HTML</button>
161
+ <button class="btn primary" id="copyWxBtn"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>复制到公众号</button>
162
+ <button class="btn primary" id="pubBtn"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4z"/></svg>发布草稿箱</button>
163
+ </div>
164
+
165
+ <div class="content">
166
+ <!-- 中栏:编辑器 -->
167
+ <section class="editor-pane">
168
+ <div class="pane-toolbar" id="toolbar">
169
+ <button class="tb-btn" data-a="bold" title="加粗"><b>B</b></button>
170
+ <button class="tb-btn" data-a="italic" title="斜体"><i style="font-style:italic">I</i></button>
171
+ <button class="tb-btn" data-a="strike" title="删除线"><s>S</s></button>
172
+ <button class="tb-btn" data-a="h1" title="一级标题">H1</button>
173
+ <button class="tb-btn" data-a="h2" title="二级标题">H2</button>
174
+ <button class="tb-btn" data-a="h3" title="三级标题">H3</button>
175
+ <span class="tb-sep"></span>
176
+ <button class="tb-btn" data-a="quote" title="引用"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M9.5 4C5.9 4 3 6.9 3 10.5V20h8v-8H7c0-2.2 1.3-4 2.5-4V4zm11 0C16.9 4 14 6.9 14 10.5V20h8v-8h-4c0-2.2 1.3-4 2.5-4V4z"/></svg></button>
177
+ <button class="tb-btn" data-a="code" title="行内代码"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/></svg></button>
178
+ <button class="tb-btn" data-a="ul" title="无序列表"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M9 6h12M9 12h12M9 18h12"/><circle cx="4.5" cy="6" r="1.4" fill="currentColor"/><circle cx="4.5" cy="12" r="1.4" fill="currentColor"/><circle cx="4.5" cy="18" r="1.4" fill="currentColor"/></svg></button>
179
+ <button class="tb-btn" data-a="ol" title="有序列表"><svg viewBox="0 0 24 24" width="15" height="15" fill="none"><path d="M11 6h10M11 12h10M11 18h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><g fill="currentColor" font-size="8" font-family="Arial,sans-serif"><text x="2.5" y="9">1</text><text x="2.5" y="15">2</text><text x="2.5" y="21">3</text></g></svg></button>
180
+ <button class="tb-btn" data-a="highlight" title="高亮"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M13.5 4.5 19.5 10 9.5 20H3.5v-6z"/><path d="M4 20h17"/></svg></button>
181
+ <span class="tb-sep"></span>
182
+ <button class="tb-btn" data-a="link" title="链接"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
183
+ <button class="tb-btn" data-a="img" title="图片"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg></button>
184
+ <button class="tb-btn" data-a="table" title="表格"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18M10 4v16"/></svg></button>
185
+ <button class="tb-btn" data-a="hr" title="分隔线"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M4 12h16"/></svg></button>
186
+ <span class="tb-sep"></span>
187
+ <button class="tb-btn" data-a="clear" title="清除格式"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
188
+ <button class="tb-btn" data-a="help" title="Markdown 帮助"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
189
+ </div>
190
+ <textarea class="markdown" id="md" spellcheck="false" placeholder="# 在这里输入 Markdown…"></textarea>
191
+ <div class="ed-status"><span id="wc">字数 0</span><span id="rt">0 分钟阅读</span><span class="grow"></span><span id="saveState">就绪</span></div>
192
+ </section>
193
+
194
+ <!-- 右栏:预览 -->
195
+ <section class="preview-pane">
196
+ <div class="preview-head"><span class="tab active" id="p1">实时预览</span><span class="tab" id="p2">手机视图</span><span class="grow"></span><span style="color:var(--muted)">390px</span></div>
197
+ <div class="preview-wrap" id="previewWrap"><div class="empty" id="pvEmpty">正在渲染…</div><iframe class="preview" id="pv" style="display:none"></iframe></div>
198
+ <div class="val-bar" id="status">就绪。</div>
199
+ </section>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ <div class="table-picker" id="tablePicker"></div>
204
+ <div class="toast" id="toast"></div>
205
+
206
+ <!-- 图床设置弹窗 -->
207
+ <div class="overlay" id="imgHostOverlay">
208
+ <div class="modal">
209
+ <div class="modal-head"><b>图床设置</b><button class="modal-close" data-close>&times;</button></div>
210
+ <div class="modal-body">
211
+ <div class="set-row">
212
+ <div class="s-txt"><b>发布时自动上传外链图到微信素材库</b><small>开启后会把正文里的外链图片搬运为微信 CDN(mmbiz.qpic.cn)链接,微信端才能显示;关闭则保留原外链 URL(可能无法显示)。</small></div>
213
+ <label class="switch"><input type="checkbox" id="relocateOpt" checked><span class="slider"></span></label>
214
+ </div>
215
+ <div class="set-row">
216
+ <div class="s-txt"><b>预览用图床</b><small>编辑器右侧预览使用本地 Chromium 渲染,不依赖网络图床;正式发布按上面开关处理。</small></div>
217
+ </div>
218
+ </div>
219
+ <div class="modal-foot"><button class="btn" data-close>关闭</button></div>
220
+ </div>
221
+ </div>
222
+
223
+ <script>
224
+ // ---- 状态 ----
225
+ let lastHtml=''; let themesCache=[]; let currentTitle='';
226
+ let history=[]; // [{id, title, md, ts}]
227
+ let activeId=null; // 当前正在编辑的历史条目 id(用于更新而非重复新增)
228
+ let coverFile=null; // 本地上传的封面 { data: base64, mime }
229
+ const $=s=>document.querySelector(s);
230
+ const toast=(m,ok=true)=>{const t=$('#toast');t.textContent=m;t.className='toast show'+(ok?'':' err');setTimeout(()=>t.className='toast',2200);};
231
+
232
+ async function api(path, body){
233
+ const opts=body?{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)}:{};
234
+ const r=await fetch(path,opts); const d=await r.json().catch(()=>({error:{message:'HTTP '+r.status}}));
235
+ if(d.error) throw new Error(d.error.message||'请求失败');
236
+ return d;
237
+ }
238
+
239
+ // ---- 主题 ---
240
+ async function loadThemes(){
241
+ const d=await api('/editor/api/themes'); themesCache=d.themes||[];
242
+ const saved=themesCache.filter(t=>t.origin==='saved'), presets=themesCache.filter(t=>t.origin!=='saved');
243
+ const sel=$('#themeSel'); const keep=sel.value||(themesCache[0]&&themesCache[0].name); sel.innerHTML='';
244
+ if(saved.length){const og=document.createElement('optgroup');og.label='我的主题';saved.forEach(t=>og.appendChild(new Option(t.name,t.name)));sel.appendChild(og);}
245
+ const pg=document.createElement('optgroup');pg.label='预置主题';presets.forEach(t=>pg.appendChild(new Option(t.name,t.name)));sel.appendChild(pg);
246
+ if(keep)sel.value=keep; else if(themesCache[0])sel.value=themesCache[0].name;
247
+ $('#themeList').innerHTML=saved.length?saved.map(t=>'<div>• '+t.name+'</div>').join(''):'<div>暂无(用「生成/保存主题」加入)</div>';
248
+ return sel.value;
249
+ }
250
+
251
+ // ---- 渲染 (防抖) ---
252
+ let timer;
253
+ async function render(){
254
+ clearTimeout(timer);
255
+ timer=setTimeout(async()=>{
256
+ const markdown=$('#md').value, theme=$('#themeSel').value;
257
+ try{
258
+ const d=await api('/editor/api/render',{markdown,theme});
259
+ lastHtml=d.html;
260
+ $('#pv').srcdoc=d.html; $('#pv').style.display=''; $('#pvEmpty').style.display='none';
261
+ const c=markdown.replace(/\s+/g,''); $('#wc').textContent='字数 '+c.length;
262
+ $('#rt').textContent=Math.max(1,Math.round(c.length/400))+' 分钟阅读';
263
+ const v=d.validation||{}; const issues=(v.issues||[]).map(i=>'['+i.rule+'] '+i.message);
264
+ setVal([{kind:v.pass?'ok':'err',text:(v.pass?'✓ 渲染完成 · 校验通过':'✗ 校验未通过')+' · HTML '+d.html.length+' 字'},...issues.map(t=>({kind:v.pass?'warn':'err',text:t}))]);
265
+ }catch(e){ setVal([{kind:'err',text:'渲染失败:'+e.message}]); }
266
+ },300);
267
+ }
268
+ function setVal(lines){ $('#status').innerHTML=lines.map(l=>'<span class="'+l.kind+'">'+l.text+'</span>').join('\n'); }
269
+
270
+ // ---- 工具栏 ----
271
+ function wrap(before,after=before){
272
+ const ta=$('#md'), s=ta.selectionStart, e=ta.selectionEnd, v=ta.value;
273
+ const sel=v.slice(s,e)||'文本';
274
+ const nv=v.slice(0,s)+before+sel+after+v.slice(e);
275
+ ta.value=nv; ta.selectionStart=s+before.length; ta.selectionEnd=s+before.length+sel.length; ta.focus(); render(); saveSnap();
276
+ }
277
+ function linePrefix(p){
278
+ const ta=$('#md'), s=ta.selectionStart, e=ta.selectionEnd, v=ta.value;
279
+ const ls=v.lastIndexOf('\n',s-1)+1, le=v.indexOf('\n',e); const end=le<0?v.length:le;
280
+ const block=v.slice(ls,end); const nvBlock=block.split('\n').map(l=>p+l).join('\n');
281
+ ta.value=v.slice(0,ls)+nvBlock+v.slice(end); ta.selectionStart=ls; ta.selectionEnd=ls+nvBlock.length; ta.focus(); render(); saveSnap();
282
+ }
283
+ const ACTIONS={
284
+ bold:()=>wrap('**'), italic:()=>wrap('*'), strike:()=>wrap('~~'), highlight:()=>wrap('=='),
285
+ code:()=>wrap('`'), link:()=>wrap('[','](https://)'), img:()=>wrap('![','](https://)'),
286
+ h1:()=>linePrefix('# '), h2:()=>linePrefix('## '), h3:()=>linePrefix('### '),
287
+ quote:()=>linePrefix('> '), ul:()=>linePrefix('- '), ol:()=>linePrefix('1. '),
288
+ hr:()=>{const ta=$('#md');ta.value=(ta.value+'\n\n---\n');render();},
289
+ clear:()=>{const ta=$('#md');const s=ta.selectionStart,e=ta.selectionEnd;ta.value=ta.value.slice(0,s)+ta.value.slice(e);render();},
290
+ help:()=>toast('Markdown 支持:**加粗**、*斜体*、==高亮==、> 引用、- 列表、| 表格 |、`代码`、```代码块```'),
291
+ };
292
+ $('#toolbar').addEventListener('click',e=>{
293
+ const btn=e.target.closest('.tb-btn'); const a=btn?.dataset?.a; if(!a) return;
294
+ if(a==='table'){ showTablePicker(btn); return; }
295
+ if(ACTIONS[a]) ACTIONS[a]();
296
+ });
297
+
298
+ // ---- Typora 式表格选择器 ----
299
+ const TBL_ROWS=8, TBL_COLS=10;
300
+ function buildTablePicker(){
301
+ const p=$('#tablePicker');
302
+ let g='';
303
+ for(let r=1;r<=TBL_ROWS;r++) for(let c=1;c<=TBL_COLS;c++) g+='<div class="tcell" data-r="'+r+'" data-c="'+c+'"></div>';
304
+ p.innerHTML='<div class="tlabel" id="tlabel">选择表格大小</div><div class="tgrid">'+g+'</div>';
305
+ p.addEventListener('mouseover',e=>{ const c=e.target.closest('.tcell'); if(!c) return; const r=+c.dataset.r, col=+c.dataset.c; p.querySelectorAll('.tcell').forEach(x=>{ x.classList.toggle('on',(+x.dataset.r<=r && +x.dataset.c<=col)); }); $('#tlabel').textContent=col+' 列 × '+r+' 行'; });
306
+ p.addEventListener('click',e=>{ const c=e.target.closest('.tcell'); if(!c) return; const r=+c.dataset.r, col=+c.dataset.c; hideTablePicker(); insertTable(col, r); });
307
+ }
308
+ function showTablePicker(btn){
309
+ hideTablePicker();
310
+ const p=$('#tablePicker'); const rect=btn.getBoundingClientRect();
311
+ p.style.left=Math.min(rect.left, window.innerWidth-280)+'px';
312
+ p.style.top=(rect.bottom+4)+'px';
313
+ p.style.display='block';
314
+ p.querySelectorAll('.tcell').forEach(x=>x.classList.remove('on'));
315
+ $('#tlabel').textContent='选择表格大小';
316
+ }
317
+ function hideTablePicker(){ $('#tablePicker').style.display='none'; }
318
+ document.addEventListener('click',e=>{ if(!e.target.closest('#tablePicker') && !e.target.closest('.tb-btn[data-a=table]')) hideTablePicker(); });
319
+ // 在光标处插入 cols 列 × rows 行(首行表头)的 Markdown 表格
320
+ function insertTable(cols, rows){
321
+ const ta=$('#md'), s=ta.selectionStart;
322
+ const mk=(v)=>'| '+Array(cols).fill(v).join(' | ')+' |';
323
+ let t='\n'+mk(' ')+'\n'+('|'+Array(cols).fill(' --- ').join('|')+'|')+'\n';
324
+ for(let i=1;i<rows;i++) t+=mk(' ')+'\n';
325
+ t+='\n';
326
+ ta.value=ta.value.slice(0,s)+t+ta.value.slice(s);
327
+ const pos=s+t.length-1; ta.selectionStart=ta.selectionEnd=pos;
328
+ ta.focus(); render(); queueSave();
329
+ setVal([{kind:'ok',text:'已插入 '+cols+' 列 × '+rows+' 行表格(首行为表头)'}]);
330
+ }
331
+
332
+ // ---- 历史记录 (localStorage) ----
333
+ let saveTimer;
334
+ // 若当前 activeId 能找到,则更新该条目(并移到顶部);否则新建一条
335
+ function saveSnap(){
336
+ if(!$('#md').value.trim()) return;
337
+ const title=$('#title').value||((($('#md').value.match(/^#\s+(.+)$/m)||[])[1])||'未命名');
338
+ const md=$('#md').value, ts=Date.now();
339
+ let i = activeId!=null ? history.findIndex(h=>h.id===activeId) : -1;
340
+ if(i<0){
341
+ const item={id:'h'+ts+Math.random().toString(36).slice(2,6), title, md, ts};
342
+ history.unshift(item); activeId=item.id; i=0;
343
+ } else {
344
+ history[i]={ ...history[i], title, md, ts };
345
+ if(i>0){ history.unshift(history.splice(i,1)[0]); } // 更新后置顶
346
+ }
347
+ history=history.slice(0,20);
348
+ localStorage.setItem('mp-history',JSON.stringify(history));
349
+ renderHistory();
350
+ $('#saveState').textContent='已自动保存';
351
+ setTimeout(()=>{ if($('#saveState').textContent==='已自动保存') $('#saveState').textContent='就绪'; },1000);
352
+ }
353
+ // 防抖:停止输入约 1.2s 才写历史
354
+ function queueSave(){ clearTimeout(saveTimer); saveTimer=setTimeout(saveSnap,1200); }
355
+ function renderHistory(filter){ const list=history.filter(h=>!filter||h.title.includes(filter)||h.md.includes(filter)); $('#histList').innerHTML=list.length?list.map(h=>'<div class="hist-item" data-id="'+h.id+'"><div class="h-title">'+esc(h.title)+'</div><div class="h-meta">'+new Date(h.ts).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'})+'</div></div>').join(''):'<div class="empty">暂无历史</div>'; }
356
+ function esc(s){return s.replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
357
+ function loadHistory(){ const raw=JSON.parse(localStorage.getItem('mp-history')||'[]'); const seen=new Set(); const out=[]; for(const h of raw){ if(!h.id) h.id='h'+(h.ts||Date.now())+Math.random().toString(36).slice(2,6); if(!seen.has(h.title)){ seen.add(h.title); out.push(h); } } history=out; }
358
+ $('#histList').addEventListener('click',e=>{const id=e.target.closest('.hist-item')?.dataset?.id; if(id){const h=history.find(x=>x.id===id); if(h){$('#md').value=h.md;$('#title').value=h.title;activeId=h.id;render();}}});
359
+ $('#search').addEventListener('input',e=>renderHistory(e.target.value.trim()));
360
+
361
+ // ---- 新建 ----
362
+ $('#newBtn').onclick=()=>{ $('#md').value=''; $('#title').value=''; $('#pv').srcdoc=''; $('#pv').style.display='none'; $('#pvEmpty').style.display=''; activeId=null; setVal([{kind:'warn',text:'新建文章:开始输入 Markdown'} ]); $('#md').focus(); };
363
+
364
+ // ---- 主题操作 ----
365
+ $('#genBtn').onclick=async()=>{ toast('正在生成主题(LLM)…'); try{ const d=await api('/editor/api/generate',{article:$('#md').value,save:true}); if(d.theme&&d.theme.name){await loadThemes();$('#themeSel').value=d.theme.name;} toast('已生成'+(d.saved?'并保存':'')+':'+(d.theme&&d.theme.name)); render(); }catch(e){ toast('生成失败:'+e.message,false); } };
366
+ $('#saveBtn').onclick=async()=>{ const name=$('#themeSel').value; const th=themesCache.find(t=>t.name===name); if(!th)return toast('请先选择主题',false); try{ await api('/editor/api/savetheme',{theme:th}); await loadThemes(); $('#themeSel').value=name; toast('已保存:'+name); }catch(e){ toast('保存失败:'+e.message,false); } };
367
+ $('#valBtn').onclick=async()=>{ const md=$('#md').value,theme=$('#themeSel').value; try{ const d=await api('/editor/api/render',{markdown:md,theme}); const v=d.validation||{}; const issues=(v.issues||[]).map(i=>'['+i.rule+'] '+i.message); setVal([{kind:v.pass?'ok':'err',text:v.pass?'✓ 校验通过':'✗ 校验未通过'},...issues.map(t=>({kind:v.pass?'warn':'err',text:t}))]); toast(v.pass?'校验通过':'校验未通过',v.pass); }catch(e){ toast('校验失败:'+e.message,false); } };
368
+
369
+ // ---- 复制 / 发布 ----
370
+ async function copyText(t,msg){ try{ await navigator.clipboard.writeText(t); toast(msg); }catch(e){ toast('复制失败(浏览器限制):'+e.message,false); } }
371
+ $('#copyHtmlBtn').onclick=()=> lastHtml?copyText(lastHtml,'已复制 HTML'):toast('请先渲染',false);
372
+ $('#copyWxBtn').onclick=()=> lastHtml?copyText(lastHtml,'已复制,去公众号编辑器粘贴'):toast('请先渲染',false);
373
+ $('#pubBtn').onclick=async()=>{ const title=$('#title').value||((($('#md').value.match(/^#\s+(.+)$/m)||[])[1])||'未命名'); toast('正在发布草稿箱…'); try{ const cover=$('#cover').value.trim(); const d=await api('/editor/api/publish',{markdown:$('#md').value,title,theme:$('#themeSel').value,relocate:$('#relocateOpt').checked,coverImage:cover||undefined,coverData:coverFile?coverFile.data:undefined,coverMime:coverFile?coverFile.mime:undefined}); toast('已发布 media_id='+(d.media_id||'').slice(0,20)+'…'); const cs=coverFile?'本地上传':(cover?'自定义URL':(d.coverMediaId?'自动':'')); setVal([{kind:'ok',text:'✅ 已发布草稿 media_id='+d.media_id+(cs?'(封面:'+cs+')':'')}]); saveSnap(); }catch(e){ toast('发布失败:'+e.message,false); } };
374
+
375
+ // ---- 侧栏导航 / 设置 / AI 优化 ----
376
+ function nav(id){ document.querySelectorAll('.subnav button').forEach(b=>b.classList.remove('active')); var b=document.getElementById(id); if(b)b.classList.add('active'); }
377
+ $('#navTheme').onclick=()=>{ nav('navTheme'); $('#themePanel').open=true; $('#themeSel').focus(); };
378
+ $('#navImgHost').onclick=()=>{ nav('navImgHost'); $('#imgHostOverlay').classList.add('show'); };
379
+ $('#navAi').onclick=async()=>{ nav('navAi'); if(!$('#md').value.trim())return toast('请先输入文章',false); toast('AI 优化中(LLM)…'); try{ const d=await api('/editor/api/optimize',{markdown:$('#md').value}); $('#md').value=d.markdown; saveSnap(); render(); toast('AI 优化完成'); }catch(e){ toast('AI 优化失败:'+e.message,false); } };
380
+ // 设置弹窗:开关
381
+ const relocateToggle=$('#relocateOpt'); relocateToggle.checked=localStorage.getItem('mp-relocate')!=='0'; relocateToggle.addEventListener('change',()=>localStorage.setItem('mp-relocate',relocateToggle.checked?'1':'0'));
382
+ document.querySelectorAll('[data-close]').forEach(b=>b.onclick=()=>{ $('#imgHostOverlay').classList.remove('show'); });
383
+ $('#imgHostOverlay').addEventListener('click',e=>{ if(e.target.id==='imgHostOverlay') $('#imgHostOverlay').classList.remove('show'); });
384
+
385
+ // ---- 标签切换 (实时预览 / 手机视图) ----
386
+ $('#p1').onclick=()=>{ $('#p1').classList.add('active'); $('#p2').classList.remove('active'); $('#previewWrap').style.display=''; $('#pv').style.width='390px'; };
387
+ $('#p2').onclick=()=>{ $('#p2').classList.add('active'); $('#p1').classList.remove('active'); $('#pv').style.width='100%'; };
388
+
389
+ // ---- 事件 ----
390
+ // ---- 封面上传 / 预览 ----
391
+ $('#coverUpload').onclick=()=>$('#coverFile').click();
392
+ $('#coverFile').addEventListener('change',e=>{
393
+ const f=e.target.files && e.target.files[0]; if(!f) return;
394
+ if(f.size > 5*1024*1024){ toast('图片过大(>5MB),请换小图',false); return; }
395
+ const reader=new FileReader();
396
+ reader.onload=()=>{ coverFile={ data:String(reader.result).split(',')[1], mime:f.type||'image/jpeg' }; $('#coverThumb').src=String(reader.result); $('#coverThumbWrp').style.display=''; $('#cover').value=''; toast('已选择本地封面'); };
397
+ reader.readAsDataURL(f);
398
+ });
399
+ $('#coverClear').onclick=()=>{ coverFile=null; $('#coverThumbWrp').style.display='none'; $('#coverThumb').src=''; $('#cover').value=''; toast('已清除封面'); };
400
+
401
+ $('#themeSel').addEventListener('change',render);
402
+ $('#md').addEventListener('input',()=>{render();queueSave();});
403
+ $('#title').addEventListener('input',queueSave);
404
+ $('#undoBtn').onclick=()=>document.execCommand('undo');
405
+ $('#redoBtn').onclick=()=>document.execCommand('redo');
406
+
407
+ // ---- 启动 ----
408
+ (async()=>{
409
+ buildTablePicker();
410
+ loadHistory(); renderHistory(); activeId=history[0]?.id ?? null;
411
+ const saved=history[0]; if(saved){ $('#md').value=saved.md; $('#title').value=saved.title; }
412
+ else { $('#md').value='# 示例标题\n\n在这里用 Markdown 写作。支持 **加粗**、*斜体*、==高亮==、> 引用、列表、表格、代码块等。\n\n## 小节一\n\n> 引用一条。\n\n- 要点一\n- 要点二\n\n| 列A | 列B |\n| --- | --- |\n| 1 | 2 |\n'; }
413
+ await loadThemes(); render();
414
+ })().catch(e=>setVal([{kind:'err',text:'初始化失败:'+e.message}]));
415
+ </script>
416
+ </body>
417
+ </html>
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@stylewx/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "stylewx MCP Server: exposes styling/validate/publish capabilities to AI agents (Kimi Code / Claude Code / Cursor). stdio + Streamable HTTP.",
5
+ "type": "module",
6
+ "bin": {
7
+ "stylewx-mcp": "dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "editor.html"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.json",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "type-check": "tsc -p tsconfig.json --noEmit",
26
+ "dev": "node --import tsx/esm src/index.ts --transport stdio",
27
+ "inspector": "node dist/index.js --transport stdio",
28
+ "prepublishOnly": "pnpm build"
29
+ },
30
+ "dependencies": {
31
+ "@stylewx/core": "workspace:*",
32
+ "@stylewx/publisher": "workspace:*",
33
+ "@stylewx/service": "workspace:*",
34
+ "@stylewx/theme": "workspace:*",
35
+ "@stylewx/validator": "workspace:*",
36
+ "@modelcontextprotocol/sdk": "^1.12.0",
37
+ "zod": "^4.0.0"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }