@zhin.js/adapter-juejin 0.1.1 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-juejin",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Zhin.js Juejin (掘金) comment adapter for Plugin Runtime (Experimental; whitelisted article comments, review-then-send with a Console approval page)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -33,22 +33,22 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "react": "^19.2.8",
36
- "@zhin.js/adapter": "1.1.28",
37
- "@zhin.js/client": "1.1.24",
36
+ "@zhin.js/adapter": "1.1.29",
37
+ "@zhin.js/client": "1.1.25",
38
38
  "@zhin.js/console-contract": "1.1.3",
39
- "@zhin.js/core": "1.1.56",
40
- "@zhin.js/feature-kit": "1.1.12",
41
- "@zhin.js/im-contract": "1.1.11",
39
+ "@zhin.js/core": "1.1.57",
40
+ "@zhin.js/feature-kit": "1.1.13",
41
+ "@zhin.js/im-contract": "1.1.12",
42
42
  "@zhin.js/logger": "1.1.2",
43
- "@zhin.js/page": "1.1.14"
43
+ "@zhin.js/page": "1.1.15"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "react": "^19.0.0",
47
- "@zhin.js/adapter": "^1.1.28",
48
- "@zhin.js/core": "^1.1.56",
49
- "zhin.js": "^1.1.21",
50
- "@zhin.js/command": "^1.1.14",
51
- "@zhin.js/page": "^1.1.14"
47
+ "@zhin.js/adapter": "^1.1.29",
48
+ "@zhin.js/core": "^1.1.57",
49
+ "zhin.js": "^1.1.22",
50
+ "@zhin.js/command": "^1.1.15",
51
+ "@zhin.js/page": "^1.1.15"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "zhin.js": {
@@ -66,8 +66,8 @@
66
66
  "@types/react": "^19.2.18",
67
67
  "typescript": "^6.0.3",
68
68
  "vitest": "^4.1.10",
69
- "@zhin.js/pagemanager": "1.1.21",
70
- "zhin.js": "1.1.21"
69
+ "@zhin.js/pagemanager": "1.1.22",
70
+ "zhin.js": "1.1.22"
71
71
  },
72
72
  "files": [
73
73
  "adapters",
@@ -1,7 +1,7 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
2
  import { definePage } from '@zhin.js/console-contract';
4
- import CommentReview from './CommentReview';
3
+ import { createCommentReviewPage } from '@zhin.js/client';
4
+ import { spec } from './spec.js';
5
5
  export const meta = definePage({
6
6
  title: '掘金评论审批',
7
7
  titleI18n: { en: 'Juejin comment review' },
@@ -11,8 +11,7 @@ export const meta = definePage({
11
11
  });
12
12
  /**
13
13
  * Convention page entry: `pages/index/index.tsx` → `/<插件路径>`(如 `/juejin`)。
14
- * 看登录态和白名单文章、审批 AI 写好的掘金评论回复。数据走插件 RPC(plugin.<pluginId>.status / 动作)。
14
+ * 看登录态和白名单文章、审批 AI 写好的掘金评论回复;页面本体是 `@zhin.js/client` 的 `createCommentReviewPage`,
15
+ * 数据走插件 RPC(plugin.<pluginId>.status / 动作)。
15
16
  */
16
- export default function JuejinReviewPage() {
17
- return _jsx(CommentReview, {});
18
- }
17
+ export default createCommentReviewPage(spec);
@@ -1,5 +1,6 @@
1
1
  import { definePage } from '@zhin.js/console-contract';
2
- import CommentReview from './CommentReview';
2
+ import { createCommentReviewPage } from '@zhin.js/client';
3
+ import { spec } from './spec.js';
3
4
 
4
5
  export const meta = definePage({
5
6
  title: '掘金评论审批',
@@ -11,8 +12,7 @@ export const meta = definePage({
11
12
 
12
13
  /**
13
14
  * Convention page entry: `pages/index/index.tsx` → `/<插件路径>`(如 `/juejin`)。
14
- * 看登录态和白名单文章、审批 AI 写好的掘金评论回复。数据走插件 RPC(plugin.<pluginId>.status / 动作)。
15
+ * 看登录态和白名单文章、审批 AI 写好的掘金评论回复;页面本体是 `@zhin.js/client` 的 `createCommentReviewPage`,
16
+ * 数据走插件 RPC(plugin.<pluginId>.status / 动作)。
15
17
  */
16
- export default function JuejinReviewPage() {
17
- return <CommentReview />;
18
- }
18
+ export default createCommentReviewPage(spec);
@@ -0,0 +1,28 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { ReviewResourceList } from '@zhin.js/client';
5
+ import { Button } from '@zhin.js/client/ui';
6
+ export const spec = {
7
+ adapter: 'juejin',
8
+ platform: { zh: '掘金', en: 'Juejin' },
9
+ mark: '掘',
10
+ resource: { zh: '文章', en: 'articles' },
11
+ openLabel: '打开文章',
12
+ account: endpoint => ({ id: endpoint.login.userId, name: endpoint.login.userName }),
13
+ draftResource: (draft, t) => t('文章 {0}', draft.articleId),
14
+ loginHint: ({ login }, t) => login.state === 'none' ? t('未登录时只能读取公开评论,不能发送回复。请用 JUEJIN_COOKIE 环境变量注入 Cookie。')
15
+ : login.state === 'expired' ? t('Cookie 已失效,草稿都保留着。更新 JUEJIN_COOKIE 后重启就能继续发送。')
16
+ : t('Cookie 只来自配置 / 环境变量,不会经过这个页面,也不会写进数据目录。'),
17
+ renderLoginExtras: (_endpoint, { t, pending, act }) => (_jsx("div", { className: "zk-row", children: _jsx(Button, { busy: pending === 'login-check', disabled: pending !== undefined, onClick: () => act('login-check'), children: t('重新检查登录') }) })),
18
+ renderResources: (endpoint, { t, noun }) => (_jsx(ReviewResourceList, { title: t('管理的{0}', noun), items: endpoint.articles.map(article => ({ key: article.id, label: article.id, url: article.url })), empty: t('还没有配置文章。要管理哪些文章请在配置的 articles 里填写文章 id 或链接。'), footnote: t('{0}列表来自配置,要增删请改配置文件。', noun) })),
19
+ dictionary: {
20
+ '打开文章': 'Open article',
21
+ '文章 {0}': 'Article {0}',
22
+ '未登录时只能读取公开评论,不能发送回复。请用 JUEJIN_COOKIE 环境变量注入 Cookie。': 'Without signing in, only public comments are read and replies cannot be posted. Inject the cookie with the JUEJIN_COOKIE environment variable.',
23
+ 'Cookie 已失效,草稿都保留着。更新 JUEJIN_COOKIE 后重启就能继续发送。': 'The cookie has expired. Drafts are kept; update JUEJIN_COOKIE and restart to keep sending.',
24
+ 'Cookie 只来自配置 / 环境变量,不会经过这个页面,也不会写进数据目录。': 'The cookie only comes from config or environment; it never passes through this page or the data directory.',
25
+ '重新检查登录': 'Check login again',
26
+ '还没有配置文章。要管理哪些文章请在配置的 articles 里填写文章 id 或链接。': 'No articles configured. Add article IDs or links under articles in the config.',
27
+ },
28
+ };
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { ReviewResourceList, type CommentReviewSpec, type ReviewDraftBase, type ReviewEndpointBase } from '@zhin.js/client';
3
+ import { Button } from '@zhin.js/client/ui';
4
+
5
+ /** 与插件 `src/console.ts` 返回的结构保持一致(只读视图)。 */
6
+ interface JuejinDraft extends ReviewDraftBase { readonly articleId: string }
7
+ interface JuejinEndpoint extends ReviewEndpointBase<JuejinDraft> {
8
+ readonly login: { readonly state: 'none' | 'ok' | 'expired'; readonly userId?: string; readonly userName?: string };
9
+ readonly articles: readonly { readonly id: string; readonly url: string }[];
10
+ }
11
+
12
+ export const spec: CommentReviewSpec<JuejinEndpoint, JuejinDraft> = {
13
+ adapter: 'juejin',
14
+ platform: { zh: '掘金', en: 'Juejin' },
15
+ mark: '掘',
16
+ resource: { zh: '文章', en: 'articles' },
17
+ openLabel: '打开文章',
18
+ account: endpoint => ({ id: endpoint.login.userId, name: endpoint.login.userName }),
19
+ draftResource: (draft, t) => t('文章 {0}', draft.articleId),
20
+ loginHint: ({ login }, t) => login.state === 'none' ? t('未登录时只能读取公开评论,不能发送回复。请用 JUEJIN_COOKIE 环境变量注入 Cookie。')
21
+ : login.state === 'expired' ? t('Cookie 已失效,草稿都保留着。更新 JUEJIN_COOKIE 后重启就能继续发送。')
22
+ : t('Cookie 只来自配置 / 环境变量,不会经过这个页面,也不会写进数据目录。'),
23
+ renderLoginExtras: (_endpoint, { t, pending, act }) => (
24
+ <div className="zk-row"><Button busy={pending === 'login-check'} disabled={pending !== undefined} onClick={() => act('login-check')}>{t('重新检查登录')}</Button></div>
25
+ ),
26
+ renderResources: (endpoint, { t, noun }) => (
27
+ <ReviewResourceList title={t('管理的{0}', noun)} items={endpoint.articles.map(article => ({ key: article.id, label: article.id, url: article.url }))}
28
+ empty={t('还没有配置文章。要管理哪些文章请在配置的 articles 里填写文章 id 或链接。')} footnote={t('{0}列表来自配置,要增删请改配置文件。', noun)} />
29
+ ),
30
+ dictionary: {
31
+ '打开文章': 'Open article',
32
+ '文章 {0}': 'Article {0}',
33
+ '未登录时只能读取公开评论,不能发送回复。请用 JUEJIN_COOKIE 环境变量注入 Cookie。': 'Without signing in, only public comments are read and replies cannot be posted. Inject the cookie with the JUEJIN_COOKIE environment variable.',
34
+ 'Cookie 已失效,草稿都保留着。更新 JUEJIN_COOKIE 后重启就能继续发送。': 'The cookie has expired. Drafts are kept; update JUEJIN_COOKIE and restart to keep sending.',
35
+ 'Cookie 只来自配置 / 环境变量,不会经过这个页面,也不会写进数据目录。': 'The cookie only comes from config or environment; it never passes through this page or the data directory.',
36
+ '重新检查登录': 'Check login again',
37
+ '还没有配置文章。要管理哪些文章请在配置的 articles 里填写文章 id 或链接。': 'No articles configured. Add article IDs or links under articles in the config.',
38
+ },
39
+ };
@@ -1,140 +0,0 @@
1
- // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
- import { adapterConsoleStyles } from '@zhin.js/client';
5
- import { call } from './rpc';
6
- import { detectLocale, text } from './messages';
7
- const COMMENT_LIMIT = 1000;
8
- function errorMessage(error, locale) {
9
- if (error instanceof Error && error.message)
10
- return error.message;
11
- return text(locale, '操作失败');
12
- }
13
- function ago(at, locale) {
14
- const minutes = Math.floor((Date.now() - at) / 60_000);
15
- if (minutes < 1)
16
- return text(locale, '刚刚');
17
- if (minutes < 60)
18
- return text(locale, '{0} 分钟前', minutes);
19
- const hours = Math.floor(minutes / 60);
20
- if (hours < 24)
21
- return text(locale, '{0} 小时前', hours);
22
- return text(locale, '{0} 天前', Math.floor(hours / 24));
23
- }
24
- function Badge({ tone, children }) {
25
- return _jsx("span", { className: `ac-badge ac-badge-${tone}`, children: children });
26
- }
27
- function Button({ children, onClick, disabled, variant = 'default', title }) {
28
- return (_jsx("button", { type: "button", title: title, disabled: disabled, onClick: onClick, className: `ac-button ac-button-${variant}`, children: children }));
29
- }
30
- function LoginPanel({ endpoint, locale, onChanged, setNotice }) {
31
- const [busy, setBusy] = useState(false);
32
- const { login } = endpoint;
33
- const recheck = async () => {
34
- setBusy(true);
35
- try {
36
- await call('login-check', { endpoint: endpoint.id, locale });
37
- onChanged();
38
- }
39
- catch (error) {
40
- setNotice({ kind: 'error', message: errorMessage(error, locale) });
41
- }
42
- finally {
43
- setBusy(false);
44
- }
45
- };
46
- const name = login.userName ? `${login.userName} · uid ${login.userId}` : login.userId ? `uid ${login.userId}` : '';
47
- return (_jsxs("section", { className: "ac-review-login", children: [_jsxs("div", { className: "ac-actions", children: [login.state === 'ok' ? _jsx(Badge, { tone: "ok", children: text(locale, '已登录') })
48
- : login.state === 'expired' ? _jsx(Badge, { tone: "warn", children: text(locale, '登录已失效') })
49
- : _jsx(Badge, { tone: "muted", children: text(locale, '未登录') }), name && _jsx("span", { className: "text-sm", children: name }), login.state !== 'none' && _jsx("span", { className: "text-xs text-muted-foreground", children: text(locale, 'Cookie 来自配置 / 环境变量') }), _jsx(Button, { disabled: busy, onClick: () => void recheck(), children: text(locale, '重新检查登录') })] }), login.state === 'none' && _jsx("p", { className: "text-sm text-muted-foreground", children: text(locale, '未登录时只能读取公开评论,不能发送回复。请用 JUEJIN_COOKIE 环境变量注入 Cookie。') }), login.state === 'expired' && (_jsx("p", { className: "text-sm text-amber-700 dark:text-amber-300", children: text(locale, 'Cookie 已失效,草稿都保留着。更新 JUEJIN_COOKIE 后重启就能继续发送。') })), _jsx("p", { className: "text-xs text-muted-foreground", children: text(locale, 'Cookie 只来自配置 / 环境变量,不会经过这个页面,也不会写进数据目录。') })] }));
50
- }
51
- function DraftCard({ draft, endpoint, locale, onChanged, setNotice }) {
52
- const [editing, setEditing] = useState(false);
53
- const [value, setValue] = useState(draft.message);
54
- const [busy, setBusy] = useState('');
55
- useEffect(() => { if (!editing)
56
- setValue(draft.message); }, [draft.message, editing]);
57
- const length = [...value].length;
58
- const sending = draft.status === 'sending' || busy === 'send';
59
- const canSend = endpoint.sendMode !== 'off' && endpoint.login.state === 'ok' && !endpoint.circuit;
60
- const act = async (kind) => {
61
- if (kind === 'drop' && !window.confirm(text(locale, '确定丢弃这条回复?不会发送。')))
62
- return;
63
- setBusy(kind);
64
- try {
65
- if (kind === 'save') {
66
- await call('edit', { id: draft.id, message: value, locale });
67
- setEditing(false);
68
- setNotice({ kind: 'ok', message: text(locale, '已保存修改') });
69
- }
70
- else if (kind === 'drop') {
71
- await call('drop', { id: draft.id, locale });
72
- setNotice({ kind: 'ok', message: text(locale, '已丢弃') });
73
- }
74
- else {
75
- if (editing && value.trim() !== draft.message) {
76
- await call('edit', { id: draft.id, message: value, locale });
77
- setEditing(false);
78
- }
79
- const result = await call('send', { id: draft.id, locale });
80
- setNotice({ kind: 'ok', message: text(locale, '已发出'), link: result.url });
81
- }
82
- onChanged();
83
- }
84
- catch (error) {
85
- setNotice({ kind: 'error', message: errorMessage(error, locale) });
86
- }
87
- finally {
88
- setBusy('');
89
- }
90
- };
91
- return (_jsxs("li", { className: "ac-draft", children: [_jsxs("div", { className: "ac-draft-meta", children: [_jsx("span", { children: text(locale, '文章 {0}', draft.articleId) }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: ago(draft.createdAt, locale) }), _jsx("a", { href: draft.url, target: "_blank", rel: "noreferrer", className: "underline-offset-2 hover:underline", children: text(locale, '打开文章') })] }), draft.interrupted && (_jsx("p", { className: "ac-warning", children: text(locale, '上次发送的结果未知,这条可能已经发出。请先到掘金文章评论区确认,避免重复回复。') })), draft.original !== undefined && (_jsxs("div", { className: "ac-draft-original", children: [_jsxs("div", { className: "ac-draft-label", children: [text(locale, '原评论'), " \u00B7 ", draft.author] }), _jsx("div", { className: "ac-draft-body", children: draft.original })] })), _jsxs("div", { className: "text-sm", children: [_jsx("div", { className: "ac-draft-label", children: text(locale, '拟回复') }), editing ? (_jsxs(_Fragment, { children: [_jsx("textarea", { "aria-label": text(locale, '拟回复'), autoFocus: true, value: value, onChange: (event) => setValue(event.target.value), rows: 4, className: "w-full rounded-md border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30" }), _jsxs("div", { className: `text-right text-xs ${length > COMMENT_LIMIT ? 'text-red-600' : 'text-muted-foreground'}`, children: [text(locale, '{0} 字', length), " / ", COMMENT_LIMIT] })] })) : _jsx("div", { className: "ac-draft-body", children: draft.message })] }), _jsxs("div", { className: "ac-actions", children: [_jsx(Button, { variant: "primary", disabled: !canSend || sending || busy !== '' || !value.trim() || length > COMMENT_LIMIT, onClick: () => act('send'), children: sending ? text(locale, '发送中…') : text(locale, '发送') }), editing
92
- ? _jsx(Button, { disabled: busy !== '' || !value.trim() || length > COMMENT_LIMIT, onClick: () => act('save'), children: text(locale, '保存') })
93
- : _jsx(Button, { disabled: busy !== '' || sending, onClick: () => setEditing(true), children: text(locale, '修改') }), editing && _jsx(Button, { disabled: busy !== '', onClick: () => { setEditing(false); setValue(draft.message); }, children: text(locale, '取消') }), _jsx(Button, { variant: "danger", disabled: busy !== '' || sending, onClick: () => act('drop'), children: text(locale, '丢弃') })] })] }));
94
- }
95
- function EndpointSection({ endpoint, locale, showTitle, onChanged, setNotice }) {
96
- return (_jsxs("div", { className: "ac-review-endpoint", children: [showTitle && _jsx("h2", { className: "text-lg font-semibold", children: text(locale, '账号 {0}', endpoint.id) }), _jsx(LoginPanel, { endpoint: endpoint, locale: locale, onChanged: onChanged, setNotice: setNotice }), endpoint.circuit && (_jsx("p", { className: "ac-notice ac-notice-error", role: "alert", children: text(locale, '触发了掘金风控或账号异常(错误码 {0}),已暂停轮询和发送。请到网页端确认账号状态,处理后重启。', endpoint.circuit.code) })), endpoint.sendMode === 'off' && _jsx("p", { className: "text-sm text-muted-foreground", children: text(locale, '出站已关闭(sendMode: off),草稿无法发送。') }), _jsxs("section", { className: "ac-review-drafts", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("h3", { className: "font-medium", children: text(locale, '待审回复') }), endpoint.pendingCount > 0 && _jsx(Badge, { tone: "warn", children: endpoint.pendingCount })] }), !endpoint.persistent && _jsx("p", { className: "text-xs text-muted-foreground", children: text(locale, '草稿只存在内存里,重启会丢失。') }), endpoint.drafts.length === 0
97
- ? _jsx("div", { className: "ac-empty", children: _jsx("p", { children: text(locale, '没有待审的回复。新的 AI 回复会自动出现在这里。') }) })
98
- : (_jsx("ul", { className: "space-y-3", children: endpoint.drafts.map(draft => (_jsx(DraftCard, { draft: draft, endpoint: endpoint, locale: locale, onChanged: onChanged, setNotice: setNotice }, draft.id))) }))] }), _jsxs("section", { className: "ac-review-videos", children: [_jsx("h3", { className: "font-medium", children: text(locale, '管理的文章') }), endpoint.articles.length === 0
99
- ? _jsx("p", { className: "text-sm text-muted-foreground", children: text(locale, '还没有配置文章。要管理哪些文章请在配置的 articles 里填写文章 id 或链接。') })
100
- : (_jsx("ul", { className: "flex flex-wrap gap-2", children: endpoint.articles.map(article => (_jsx("li", { children: _jsx("a", { href: article.url, target: "_blank", rel: "noreferrer", className: "rounded-md border px-2 py-1 text-sm hover:bg-accent", children: article.id }) }, article.id))) })), _jsx("p", { className: "text-xs text-muted-foreground", children: text(locale, '文章列表来自配置,要增删请改配置文件。') })] })] }));
101
- }
102
- export default function CommentReview() {
103
- const locale = useMemo(detectLocale, []);
104
- const [status, setStatus] = useState();
105
- const [loadError, setLoadError] = useState();
106
- const [notice, setNotice] = useState();
107
- const timer = useRef(undefined);
108
- const refresh = useCallback(async () => {
109
- try {
110
- const next = await call('status');
111
- setStatus(next);
112
- setLoadError(undefined);
113
- }
114
- catch (error) {
115
- setLoadError(errorMessage(error, locale));
116
- }
117
- }, [locale]);
118
- useEffect(() => {
119
- let cancelled = false;
120
- const loop = async () => {
121
- if (cancelled)
122
- return;
123
- if (typeof document === 'undefined' || document.visibilityState !== 'hidden')
124
- await refresh();
125
- if (!cancelled)
126
- timer.current = setTimeout(loop, 10_000);
127
- };
128
- void loop();
129
- return () => { cancelled = true; if (timer.current)
130
- clearTimeout(timer.current); };
131
- }, [refresh]);
132
- useEffect(() => {
133
- if (!notice || notice.kind === 'error')
134
- return;
135
- const id = setTimeout(() => setNotice(undefined), 6_000);
136
- return () => clearTimeout(id);
137
- }, [notice]);
138
- const endpoints = status?.endpoints ?? [];
139
- return (_jsxs("div", { className: "zhin-console-surface zhin-review-page", children: [_jsx("style", { children: adapterConsoleStyles }), _jsxs("header", { className: "ac-header", children: [_jsxs("div", { className: "ac-identity", children: [_jsx("span", { className: "ac-platform-mark", "aria-hidden": "true", children: "\u6398" }), _jsxs("div", { children: [_jsx("h1", { children: text(locale, '掘金评论审批') }), _jsx("p", { children: text(locale, 'AI 写好的掘金评论回复先停在这里,你点「发送」才会真的发出去。') })] })] }), _jsx(Button, { onClick: () => void refresh(), children: text(locale, '刷新') })] }), notice && (_jsxs("div", { role: notice.kind === 'ok' ? 'status' : 'alert', className: `ac-notice ${notice.kind === 'ok' ? '' : 'ac-notice-error'}`, children: [notice.message, notice.link && _jsxs(_Fragment, { children: [" \u00B7 ", _jsx("a", { href: notice.link, target: "_blank", rel: "noreferrer", className: "underline", children: text(locale, '查看') })] })] })), loadError && _jsx("p", { role: "alert", className: "ac-notice ac-notice-error", children: loadError }), !status && !loadError && _jsxs("div", { className: "ac-loading", role: "status", "aria-busy": "true", children: [_jsx("p", { children: text(locale, '加载中…') }), _jsx("span", { "aria-hidden": "true" })] }), status && endpoints.length === 0 && _jsx("div", { className: "ac-empty", children: _jsx("p", { children: text(locale, '掘金适配器还没有启动。请先在配置里添加 juejin 插件和至少一个账号。') }) }), endpoints.map(endpoint => (_jsx(EndpointSection, { endpoint: endpoint, locale: locale, showTitle: endpoints.length > 1, onChanged: () => void refresh(), setNotice: setNotice }, endpoint.id)))] }));
140
- }
@@ -1,264 +0,0 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { adapterConsoleStyles } from '@zhin.js/client';
3
- import { call, type ReviewDraft, type ReviewEndpoint, type ReviewStatus } from './rpc';
4
- import { detectLocale, text, type ReviewLocale } from './messages';
5
-
6
- const COMMENT_LIMIT = 1000;
7
-
8
- type Notice = { readonly kind: 'ok' | 'error'; readonly message: string; readonly link?: string };
9
-
10
- function errorMessage(error: unknown, locale: ReviewLocale): string {
11
- if (error instanceof Error && error.message) return error.message;
12
- return text(locale, '操作失败');
13
- }
14
-
15
- function ago(at: number, locale: ReviewLocale): string {
16
- const minutes = Math.floor((Date.now() - at) / 60_000);
17
- if (minutes < 1) return text(locale, '刚刚');
18
- if (minutes < 60) return text(locale, '{0} 分钟前', minutes);
19
- const hours = Math.floor(minutes / 60);
20
- if (hours < 24) return text(locale, '{0} 小时前', hours);
21
- return text(locale, '{0} 天前', Math.floor(hours / 24));
22
- }
23
-
24
- function Badge({ tone, children }: { tone: 'ok' | 'warn' | 'muted'; children: React.ReactNode }) {
25
- return <span className={`ac-badge ac-badge-${tone}`}>{children}</span>;
26
- }
27
-
28
- function Button({ children, onClick, disabled, variant = 'default', title }: {
29
- children: React.ReactNode; onClick?: () => void; disabled?: boolean; variant?: 'default' | 'primary' | 'danger'; title?: string;
30
- }) {
31
- return (
32
- <button type="button" title={title} disabled={disabled} onClick={onClick}
33
- className={`ac-button ac-button-${variant}`}>
34
- {children}
35
- </button>
36
- );
37
- }
38
-
39
- function LoginPanel({ endpoint, locale, onChanged, setNotice }: {
40
- endpoint: ReviewEndpoint; locale: ReviewLocale; onChanged: () => void; setNotice: (notice: Notice) => void;
41
- }) {
42
- const [busy, setBusy] = useState(false);
43
- const { login } = endpoint;
44
- const recheck = async () => {
45
- setBusy(true);
46
- try {
47
- await call('login-check', { endpoint: endpoint.id, locale });
48
- onChanged();
49
- } catch (error) {
50
- setNotice({ kind: 'error', message: errorMessage(error, locale) });
51
- } finally {
52
- setBusy(false);
53
- }
54
- };
55
- const name = login.userName ? `${login.userName} · uid ${login.userId}` : login.userId ? `uid ${login.userId}` : '';
56
- return (
57
- <section className="ac-review-login">
58
- <div className="ac-actions">
59
- {login.state === 'ok' ? <Badge tone="ok">{text(locale, '已登录')}</Badge>
60
- : login.state === 'expired' ? <Badge tone="warn">{text(locale, '登录已失效')}</Badge>
61
- : <Badge tone="muted">{text(locale, '未登录')}</Badge>}
62
- {name && <span className="text-sm">{name}</span>}
63
- {login.state !== 'none' && <span className="text-xs text-muted-foreground">{text(locale, 'Cookie 来自配置 / 环境变量')}</span>}
64
- <Button disabled={busy} onClick={() => void recheck()}>{text(locale, '重新检查登录')}</Button>
65
- </div>
66
- {login.state === 'none' && <p className="text-sm text-muted-foreground">{text(locale, '未登录时只能读取公开评论,不能发送回复。请用 JUEJIN_COOKIE 环境变量注入 Cookie。')}</p>}
67
- {login.state === 'expired' && (
68
- <p className="text-sm text-amber-700 dark:text-amber-300">{text(locale, 'Cookie 已失效,草稿都保留着。更新 JUEJIN_COOKIE 后重启就能继续发送。')}</p>
69
- )}
70
- <p className="text-xs text-muted-foreground">{text(locale, 'Cookie 只来自配置 / 环境变量,不会经过这个页面,也不会写进数据目录。')}</p>
71
- </section>
72
- );
73
- }
74
-
75
- function DraftCard({ draft, endpoint, locale, onChanged, setNotice }: {
76
- draft: ReviewDraft; endpoint: ReviewEndpoint; locale: ReviewLocale; onChanged: () => void; setNotice: (notice: Notice) => void;
77
- }) {
78
- const [editing, setEditing] = useState(false);
79
- const [value, setValue] = useState(draft.message);
80
- const [busy, setBusy] = useState<'' | 'send' | 'save' | 'drop'>('');
81
- useEffect(() => { if (!editing) setValue(draft.message); }, [draft.message, editing]);
82
- const length = [...value].length;
83
- const sending = draft.status === 'sending' || busy === 'send';
84
- const canSend = endpoint.sendMode !== 'off' && endpoint.login.state === 'ok' && !endpoint.circuit;
85
-
86
- const act = async (kind: 'send' | 'save' | 'drop') => {
87
- if (kind === 'drop' && !window.confirm(text(locale, '确定丢弃这条回复?不会发送。'))) return;
88
- setBusy(kind);
89
- try {
90
- if (kind === 'save') {
91
- await call('edit', { id: draft.id, message: value, locale });
92
- setEditing(false);
93
- setNotice({ kind: 'ok', message: text(locale, '已保存修改') });
94
- } else if (kind === 'drop') {
95
- await call('drop', { id: draft.id, locale });
96
- setNotice({ kind: 'ok', message: text(locale, '已丢弃') });
97
- } else {
98
- if (editing && value.trim() !== draft.message) {
99
- await call('edit', { id: draft.id, message: value, locale });
100
- setEditing(false);
101
- }
102
- const result = await call<{ url: string }>('send', { id: draft.id, locale });
103
- setNotice({ kind: 'ok', message: text(locale, '已发出'), link: result.url });
104
- }
105
- onChanged();
106
- } catch (error) {
107
- setNotice({ kind: 'error', message: errorMessage(error, locale) });
108
- } finally {
109
- setBusy('');
110
- }
111
- };
112
-
113
- return (
114
- <li className="ac-draft">
115
- <div className="ac-draft-meta">
116
- <span>{text(locale, '文章 {0}', draft.articleId)}</span>
117
- <span>·</span>
118
- <span>{ago(draft.createdAt, locale)}</span>
119
- <a href={draft.url} target="_blank" rel="noreferrer" className="underline-offset-2 hover:underline">{text(locale, '打开文章')}</a>
120
- </div>
121
- {draft.interrupted && (
122
- <p className="ac-warning">{text(locale, '上次发送的结果未知,这条可能已经发出。请先到掘金文章评论区确认,避免重复回复。')}</p>
123
- )}
124
- {draft.original !== undefined && (
125
- <div className="ac-draft-original">
126
- <div className="ac-draft-label">{text(locale, '原评论')} · {draft.author}</div>
127
- <div className="ac-draft-body">{draft.original}</div>
128
- </div>
129
- )}
130
- <div className="text-sm">
131
- <div className="ac-draft-label">{text(locale, '拟回复')}</div>
132
- {editing ? (
133
- <>
134
- <textarea aria-label={text(locale, '拟回复')} autoFocus value={value} onChange={(event) => setValue(event.target.value)} rows={4}
135
- className="w-full rounded-md border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30" />
136
- <div className={`text-right text-xs ${length > COMMENT_LIMIT ? 'text-red-600' : 'text-muted-foreground'}`}>{text(locale, '{0} 字', length)} / {COMMENT_LIMIT}</div>
137
- </>
138
- ) : <div className="ac-draft-body">{draft.message}</div>}
139
- </div>
140
- <div className="ac-actions">
141
- <Button variant="primary" disabled={!canSend || sending || busy !== '' || !value.trim() || length > COMMENT_LIMIT} onClick={() => act('send')}>
142
- {sending ? text(locale, '发送中…') : text(locale, '发送')}
143
- </Button>
144
- {editing
145
- ? <Button disabled={busy !== '' || !value.trim() || length > COMMENT_LIMIT} onClick={() => act('save')}>{text(locale, '保存')}</Button>
146
- : <Button disabled={busy !== '' || sending} onClick={() => setEditing(true)}>{text(locale, '修改')}</Button>}
147
- {editing && <Button disabled={busy !== ''} onClick={() => { setEditing(false); setValue(draft.message); }}>{text(locale, '取消')}</Button>}
148
- <Button variant="danger" disabled={busy !== '' || sending} onClick={() => act('drop')}>{text(locale, '丢弃')}</Button>
149
- </div>
150
- </li>
151
- );
152
- }
153
-
154
- function EndpointSection({ endpoint, locale, showTitle, onChanged, setNotice }: {
155
- endpoint: ReviewEndpoint; locale: ReviewLocale; showTitle: boolean; onChanged: () => void; setNotice: (notice: Notice) => void;
156
- }) {
157
- return (
158
- <div className="ac-review-endpoint">
159
- {showTitle && <h2 className="text-lg font-semibold">{text(locale, '账号 {0}', endpoint.id)}</h2>}
160
- <LoginPanel endpoint={endpoint} locale={locale} onChanged={onChanged} setNotice={setNotice} />
161
- {endpoint.circuit && (
162
- <p className="ac-notice ac-notice-error" role="alert">
163
- {text(locale, '触发了掘金风控或账号异常(错误码 {0}),已暂停轮询和发送。请到网页端确认账号状态,处理后重启。', endpoint.circuit.code)}
164
- </p>
165
- )}
166
- {endpoint.sendMode === 'off' && <p className="text-sm text-muted-foreground">{text(locale, '出站已关闭(sendMode: off),草稿无法发送。')}</p>}
167
-
168
- <section className="ac-review-drafts">
169
- <div className="flex items-center gap-2">
170
- <h3 className="font-medium">{text(locale, '待审回复')}</h3>
171
- {endpoint.pendingCount > 0 && <Badge tone="warn">{endpoint.pendingCount}</Badge>}
172
- </div>
173
- {!endpoint.persistent && <p className="text-xs text-muted-foreground">{text(locale, '草稿只存在内存里,重启会丢失。')}</p>}
174
- {endpoint.drafts.length === 0
175
- ? <div className="ac-empty"><p>{text(locale, '没有待审的回复。新的 AI 回复会自动出现在这里。')}</p></div>
176
- : (
177
- <ul className="space-y-3">
178
- {endpoint.drafts.map(draft => (
179
- <DraftCard key={draft.id} draft={draft} endpoint={endpoint} locale={locale} onChanged={onChanged} setNotice={setNotice} />
180
- ))}
181
- </ul>
182
- )}
183
- </section>
184
-
185
- <section className="ac-review-videos">
186
- <h3 className="font-medium">{text(locale, '管理的文章')}</h3>
187
- {endpoint.articles.length === 0
188
- ? <p className="text-sm text-muted-foreground">{text(locale, '还没有配置文章。要管理哪些文章请在配置的 articles 里填写文章 id 或链接。')}</p>
189
- : (
190
- <ul className="flex flex-wrap gap-2">
191
- {endpoint.articles.map(article => (
192
- <li key={article.id}>
193
- <a href={article.url} target="_blank" rel="noreferrer" className="rounded-md border px-2 py-1 text-sm hover:bg-accent">{article.id}</a>
194
- </li>
195
- ))}
196
- </ul>
197
- )}
198
- <p className="text-xs text-muted-foreground">{text(locale, '文章列表来自配置,要增删请改配置文件。')}</p>
199
- </section>
200
- </div>
201
- );
202
- }
203
-
204
- export default function CommentReview() {
205
- const locale = useMemo(detectLocale, []);
206
- const [status, setStatus] = useState<ReviewStatus | undefined>();
207
- const [loadError, setLoadError] = useState<string | undefined>();
208
- const [notice, setNotice] = useState<Notice | undefined>();
209
- const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
210
-
211
- const refresh = useCallback(async () => {
212
- try {
213
- const next = await call<ReviewStatus>('status');
214
- setStatus(next);
215
- setLoadError(undefined);
216
- } catch (error) {
217
- setLoadError(errorMessage(error, locale));
218
- }
219
- }, [locale]);
220
-
221
- useEffect(() => {
222
- let cancelled = false;
223
- const loop = async () => {
224
- if (cancelled) return;
225
- if (typeof document === 'undefined' || document.visibilityState !== 'hidden') await refresh();
226
- if (!cancelled) timer.current = setTimeout(loop, 10_000);
227
- };
228
- void loop();
229
- return () => { cancelled = true; if (timer.current) clearTimeout(timer.current); };
230
- }, [refresh]);
231
-
232
- useEffect(() => {
233
- if (!notice || notice.kind === 'error') return;
234
- const id = setTimeout(() => setNotice(undefined), 6_000);
235
- return () => clearTimeout(id);
236
- }, [notice]);
237
-
238
- const endpoints = status?.endpoints ?? [];
239
- return (
240
- <div className="zhin-console-surface zhin-review-page">
241
- <style>{adapterConsoleStyles}</style>
242
- <header className="ac-header">
243
- <div className="ac-identity">
244
- <span className="ac-platform-mark" aria-hidden="true">掘</span>
245
- <div><h1>{text(locale, '掘金评论审批')}</h1><p>{text(locale, 'AI 写好的掘金评论回复先停在这里,你点「发送」才会真的发出去。')}</p></div>
246
- </div>
247
- <Button onClick={() => void refresh()}>{text(locale, '刷新')}</Button>
248
- </header>
249
- {notice && (
250
- <div role={notice.kind === 'ok' ? 'status' : 'alert'} className={`ac-notice ${notice.kind === 'ok' ? '' : 'ac-notice-error'}`}>
251
- {notice.message}
252
- {notice.link && <> · <a href={notice.link} target="_blank" rel="noreferrer" className="underline">{text(locale, '查看')}</a></>}
253
- </div>
254
- )}
255
- {loadError && <p role="alert" className="ac-notice ac-notice-error">{loadError}</p>}
256
- {!status && !loadError && <div className="ac-loading" role="status" aria-busy="true"><p>{text(locale, '加载中…')}</p><span aria-hidden="true" /></div>}
257
- {status && endpoints.length === 0 && <div className="ac-empty"><p>{text(locale, '掘金适配器还没有启动。请先在配置里添加 juejin 插件和至少一个账号。')}</p></div>}
258
- {endpoints.map(endpoint => (
259
- <EndpointSection key={endpoint.id} endpoint={endpoint} locale={locale} showTitle={endpoints.length > 1}
260
- onChanged={() => void refresh()} setNotice={setNotice} />
261
- ))}
262
- </div>
263
- );
264
- }
@@ -1,57 +0,0 @@
1
- // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- const english = {
3
- '掘金评论审批': 'Juejin comment review',
4
- 'AI 写好的掘金评论回复先停在这里,你点「发送」才会真的发出去。': 'AI-written Juejin replies wait here. Nothing is posted until you press Send.',
5
- '加载中…': 'Loading…',
6
- '掘金适配器还没有启动。请先在配置里添加 juejin 插件和至少一个账号。': 'The Juejin adapter has not started. Add the juejin plugin and at least one account to the config first.',
7
- '账号 {0}': 'Account {0}',
8
- '已登录': 'Signed in',
9
- '未登录': 'Not signed in',
10
- '登录已失效': 'Login expired',
11
- 'Cookie 来自配置 / 环境变量': 'Cookie from config or environment',
12
- '未登录时只能读取公开评论,不能发送回复。请用 JUEJIN_COOKIE 环境变量注入 Cookie。': 'Without signing in, only public comments are read and replies cannot be posted. Inject the cookie with the JUEJIN_COOKIE environment variable.',
13
- 'Cookie 已失效,草稿都保留着。更新 JUEJIN_COOKIE 后重启就能继续发送。': 'The cookie has expired. Drafts are kept; update JUEJIN_COOKIE and restart to keep sending.',
14
- '重新检查登录': 'Check login again',
15
- 'Cookie 只来自配置 / 环境变量,不会经过这个页面,也不会写进数据目录。': 'The cookie only comes from config or environment; it never passes through this page or the data directory.',
16
- '触发了掘金风控或账号异常(错误码 {0}),已暂停轮询和发送。请到网页端确认账号状态,处理后重启。': 'Juejin risk control or an account problem was detected (code {0}). Polling and sending are paused. Check the account on the website, then restart.',
17
- '出站已关闭(sendMode: off),草稿无法发送。': 'Outbound is off (sendMode: off); drafts cannot be sent.',
18
- '管理的文章': 'Watched articles',
19
- '还没有配置文章。要管理哪些文章请在配置的 articles 里填写文章 id 或链接。': 'No articles configured. Add article IDs or links under articles in the config.',
20
- '文章列表来自配置,要增删请改配置文件。': 'This list comes from the config; edit the config file to change it.',
21
- '待审回复': 'Replies awaiting review',
22
- '没有待审的回复。新的 AI 回复会自动出现在这里。': 'No replies awaiting review. New AI replies show up here automatically.',
23
- '草稿只存在内存里,重启会丢失。': 'Drafts are kept in memory only and are lost on restart.',
24
- '原评论': 'Original comment',
25
- '拟回复': 'Proposed reply',
26
- '上次发送的结果未知,这条可能已经发出。请先到掘金文章评论区确认,避免重复回复。': 'The last send outcome is unknown, so this may already be posted. Check the Juejin article comments first to avoid a duplicate.',
27
- '发送中…': 'Sending…',
28
- '发送': 'Send',
29
- '修改': 'Edit',
30
- '保存': 'Save',
31
- '取消': 'Cancel',
32
- '丢弃': 'Discard',
33
- '确定丢弃这条回复?不会发送。': 'Discard this reply? It will not be posted.',
34
- '打开文章': 'Open article',
35
- '已发出': 'Posted',
36
- '查看': 'View',
37
- '已丢弃': 'Discarded',
38
- '已保存修改': 'Changes saved',
39
- '操作失败': 'Action failed',
40
- '{0} 字': '{0} chars',
41
- '刚刚': 'just now',
42
- '{0} 分钟前': '{0} min ago',
43
- '{0} 小时前': '{0} h ago',
44
- '{0} 天前': '{0} d ago',
45
- '刷新': 'Refresh',
46
- '文章 {0}': 'Article {0}',
47
- };
48
- export function detectLocale() {
49
- if (typeof document === 'undefined')
50
- return 'zh-CN';
51
- const lang = document.documentElement.lang || (typeof navigator !== 'undefined' ? navigator.language : '');
52
- return /^en\b/i.test(lang) ? 'en' : 'zh-CN';
53
- }
54
- export function text(locale, key, ...values) {
55
- const template = locale === 'en' ? english[key] ?? key : key;
56
- return template.replace(/\{(\d+)\}/g, (_match, index) => String(values[Number(index)] ?? ''));
57
- }
@@ -1,59 +0,0 @@
1
- export type ReviewLocale = 'zh-CN' | 'en';
2
-
3
- const english: Readonly<Record<string, string>> = {
4
- '掘金评论审批': 'Juejin comment review',
5
- 'AI 写好的掘金评论回复先停在这里,你点「发送」才会真的发出去。': 'AI-written Juejin replies wait here. Nothing is posted until you press Send.',
6
- '加载中…': 'Loading…',
7
- '掘金适配器还没有启动。请先在配置里添加 juejin 插件和至少一个账号。': 'The Juejin adapter has not started. Add the juejin plugin and at least one account to the config first.',
8
- '账号 {0}': 'Account {0}',
9
- '已登录': 'Signed in',
10
- '未登录': 'Not signed in',
11
- '登录已失效': 'Login expired',
12
- 'Cookie 来自配置 / 环境变量': 'Cookie from config or environment',
13
- '未登录时只能读取公开评论,不能发送回复。请用 JUEJIN_COOKIE 环境变量注入 Cookie。': 'Without signing in, only public comments are read and replies cannot be posted. Inject the cookie with the JUEJIN_COOKIE environment variable.',
14
- 'Cookie 已失效,草稿都保留着。更新 JUEJIN_COOKIE 后重启就能继续发送。': 'The cookie has expired. Drafts are kept; update JUEJIN_COOKIE and restart to keep sending.',
15
- '重新检查登录': 'Check login again',
16
- 'Cookie 只来自配置 / 环境变量,不会经过这个页面,也不会写进数据目录。': 'The cookie only comes from config or environment; it never passes through this page or the data directory.',
17
- '触发了掘金风控或账号异常(错误码 {0}),已暂停轮询和发送。请到网页端确认账号状态,处理后重启。': 'Juejin risk control or an account problem was detected (code {0}). Polling and sending are paused. Check the account on the website, then restart.',
18
- '出站已关闭(sendMode: off),草稿无法发送。': 'Outbound is off (sendMode: off); drafts cannot be sent.',
19
- '管理的文章': 'Watched articles',
20
- '还没有配置文章。要管理哪些文章请在配置的 articles 里填写文章 id 或链接。': 'No articles configured. Add article IDs or links under articles in the config.',
21
- '文章列表来自配置,要增删请改配置文件。': 'This list comes from the config; edit the config file to change it.',
22
- '待审回复': 'Replies awaiting review',
23
- '没有待审的回复。新的 AI 回复会自动出现在这里。': 'No replies awaiting review. New AI replies show up here automatically.',
24
- '草稿只存在内存里,重启会丢失。': 'Drafts are kept in memory only and are lost on restart.',
25
- '原评论': 'Original comment',
26
- '拟回复': 'Proposed reply',
27
- '上次发送的结果未知,这条可能已经发出。请先到掘金文章评论区确认,避免重复回复。': 'The last send outcome is unknown, so this may already be posted. Check the Juejin article comments first to avoid a duplicate.',
28
- '发送中…': 'Sending…',
29
- '发送': 'Send',
30
- '修改': 'Edit',
31
- '保存': 'Save',
32
- '取消': 'Cancel',
33
- '丢弃': 'Discard',
34
- '确定丢弃这条回复?不会发送。': 'Discard this reply? It will not be posted.',
35
- '打开文章': 'Open article',
36
- '已发出': 'Posted',
37
- '查看': 'View',
38
- '已丢弃': 'Discarded',
39
- '已保存修改': 'Changes saved',
40
- '操作失败': 'Action failed',
41
- '{0} 字': '{0} chars',
42
- '刚刚': 'just now',
43
- '{0} 分钟前': '{0} min ago',
44
- '{0} 小时前': '{0} h ago',
45
- '{0} 天前': '{0} d ago',
46
- '刷新': 'Refresh',
47
- '文章 {0}': 'Article {0}',
48
- };
49
-
50
- export function detectLocale(): ReviewLocale {
51
- if (typeof document === 'undefined') return 'zh-CN';
52
- const lang = document.documentElement.lang || (typeof navigator !== 'undefined' ? navigator.language : '');
53
- return /^en\b/i.test(lang) ? 'en' : 'zh-CN';
54
- }
55
-
56
- export function text(locale: ReviewLocale, key: string, ...values: Array<string | number>): string {
57
- const template = locale === 'en' ? english[key] ?? key : key;
58
- return template.replace(/\{(\d+)\}/g, (_match, index: string) => String(values[Number(index)] ?? ''));
59
- }
@@ -1,31 +0,0 @@
1
- // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { apiFetch, pluginRpc } from '@zhin.js/client';
3
- const FALLBACK_PLUGIN_ID = 'root/juejin';
4
- /** 进程内缓存:用 const 容器持有,避免模块级可变绑定(adapter endpoint 边界检查)。 */
5
- const pluginIdCache = {};
6
- /**
7
- * 找到本插件的 PluginId(插件实例名可以不是 juejin):从页面清单里挑出本包的 index 页,
8
- * 优先匹配当前地址;找不到时退回 `root/juejin`。
9
- */
10
- export function resolvePluginId() {
11
- pluginIdCache.promise ??= (async () => {
12
- try {
13
- const response = await apiFetch('/console/api/pages');
14
- if (!response.ok)
15
- return FALLBACK_PLUGIN_ID;
16
- const body = await response.json();
17
- const candidates = (body.pages ?? []).filter(page => page.localName === 'index' && typeof page.owner === 'string'
18
- && typeof page.source === 'string' && /adapter-juejin|adapters[\\/]juejin[\\/]pages/.test(page.source));
19
- const here = typeof location === 'undefined' ? '' : location.pathname.replace(/\/+$/, '') || '/';
20
- const match = candidates.find(page => page.route === here) ?? candidates[0];
21
- return match?.owner ?? FALLBACK_PLUGIN_ID;
22
- }
23
- catch {
24
- return FALLBACK_PLUGIN_ID;
25
- }
26
- })();
27
- return pluginIdCache.promise;
28
- }
29
- export async function call(method, data) {
30
- return pluginRpc(await resolvePluginId(), method, data);
31
- }
@@ -1,72 +0,0 @@
1
- import { apiFetch, pluginRpc } from '@zhin.js/client';
2
-
3
- /** 与插件 `src/console.ts` 返回的结构保持一致(只读视图)。 */
4
- export interface ReviewDraft {
5
- readonly id: string;
6
- readonly url: string;
7
- readonly articleId: string;
8
- readonly message: string;
9
- readonly status: 'pending' | 'sending';
10
- readonly interrupted: boolean;
11
- readonly createdAt: number;
12
- readonly updatedAt?: number;
13
- readonly author?: string;
14
- readonly original?: string;
15
- }
16
-
17
- export interface ReviewEndpoint {
18
- readonly id: string;
19
- readonly sendMode: 'review' | 'off';
20
- readonly persistent: boolean;
21
- readonly circuit: { readonly code: number } | null;
22
- readonly login: {
23
- readonly state: 'none' | 'ok' | 'expired';
24
- readonly userId?: string;
25
- readonly userName?: string;
26
- };
27
- readonly articles: readonly { readonly id: string; readonly url: string }[];
28
- readonly pendingCount: number;
29
- readonly drafts: readonly ReviewDraft[];
30
- }
31
-
32
- export interface ReviewStatus {
33
- readonly endpoints: readonly ReviewEndpoint[];
34
- readonly updatedAt: number;
35
- }
36
-
37
- interface PageEntry {
38
- readonly owner?: string;
39
- readonly localName?: string;
40
- readonly source?: string;
41
- readonly route?: string;
42
- }
43
-
44
- const FALLBACK_PLUGIN_ID = 'root/juejin';
45
- /** 进程内缓存:用 const 容器持有,避免模块级可变绑定(adapter endpoint 边界检查)。 */
46
- const pluginIdCache: { promise?: Promise<string> } = {};
47
-
48
- /**
49
- * 找到本插件的 PluginId(插件实例名可以不是 juejin):从页面清单里挑出本包的 index 页,
50
- * 优先匹配当前地址;找不到时退回 `root/juejin`。
51
- */
52
- export function resolvePluginId(): Promise<string> {
53
- pluginIdCache.promise ??= (async () => {
54
- try {
55
- const response = await apiFetch('/console/api/pages');
56
- if (!response.ok) return FALLBACK_PLUGIN_ID;
57
- const body = await response.json() as { pages?: readonly PageEntry[] };
58
- const candidates = (body.pages ?? []).filter(page => page.localName === 'index' && typeof page.owner === 'string'
59
- && typeof page.source === 'string' && /adapter-juejin|adapters[\\/]juejin[\\/]pages/.test(page.source));
60
- const here = typeof location === 'undefined' ? '' : location.pathname.replace(/\/+$/, '') || '/';
61
- const match = candidates.find(page => page.route === here) ?? candidates[0];
62
- return match?.owner ?? FALLBACK_PLUGIN_ID;
63
- } catch {
64
- return FALLBACK_PLUGIN_ID;
65
- }
66
- })();
67
- return pluginIdCache.promise;
68
- }
69
-
70
- export async function call<T>(method: string, data?: Record<string, unknown>): Promise<T> {
71
- return pluginRpc<T>(await resolvePluginId(), method, data);
72
- }