dsh-plugin-subscriptions 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,20 @@ English | [中文](README.zh.md)
4
4
 
5
5
  Use your **ChatGPT (Codex)**, **Claude**, and **Grok (X Premium)** subscriptions as LLM providers in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — no API keys. Login happens in the dsh web UI (Settings → Subscriptions); tokens live at `~/.dsh/plugins/subscriptions/auth.json` (mode 0600) and refresh automatically.
6
6
 
7
+ ## Demo
8
+
9
+ Settings → **Subscriptions**: per-provider OAuth login/logout, no API keys (account address masked in the screenshot):
10
+
11
+ ![Subscriptions settings page](https://raw.githubusercontent.com/V1ki/dsh-plugin-subscriptions/main/docs/images/subscriptions.png)
12
+
13
+ Logged-in providers join the session model picker with their live model catalogs:
14
+
15
+ ![Model picker with subscription models](https://raw.githubusercontent.com/V1ki/dsh-plugin-subscriptions/main/docs/images/model-picker.png)
16
+
17
+ The `image_generate` tool renders its result inline in the conversation:
18
+
19
+ ![image_generate renders the image inline](https://raw.githubusercontent.com/V1ki/dsh-plugin-subscriptions/main/docs/images/image-generate-inline.png)
20
+
7
21
  ## Providers
8
22
 
9
23
  | Route | Subscription | Models |
@@ -21,7 +35,13 @@ Also included, registered when the matching provider is enabled:
21
35
 
22
36
  ## Install
23
37
 
24
- With the `dsh` CLI available:
38
+ With the `dsh` CLI available, install from npm (prebuilt artifacts, no build permission needed):
39
+
40
+ ```sh
41
+ dsh plugin --profile web add dsh-plugin-subscriptions
42
+ ```
43
+
44
+ Or install the sources from GitHub:
25
45
 
26
46
  ```sh
27
47
  dsh plugin --profile web add github:V1ki/dsh-plugin-subscriptions
@@ -47,6 +67,7 @@ dsh plugin --profile web add ./dsh-plugin-subscriptions
47
67
  Headless-only usage without installing into a profile (log in via the web UI first — the token file is shared):
48
68
 
49
69
  ```sh
70
+ cp overlay.example.yml overlay.yml # then edit the name: to this checkout's absolute lib/index.js path
50
71
  dsh --profile headless --patch <checkout>/overlay.yml "your task"
51
72
  ```
52
73
 
package/README.zh.md CHANGED
@@ -4,6 +4,20 @@
4
4
 
5
5
  把你的 **ChatGPT(Codex)**、**Claude**、**Grok(X Premium)**订阅当作 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的 LLM provider 使用 —— 不需要 API key。登录在 dsh web 界面完成(设置 → 订阅);token 保存在 `~/.dsh/plugins/subscriptions/auth.json`(权限 0600),过期自动刷新。
6
6
 
7
+ ## 演示
8
+
9
+ 设置 → **订阅**:每个 provider 的 OAuth 登录/退出,无需 API key(截图中账号已打码):
10
+
11
+ ![订阅设置页](https://raw.githubusercontent.com/V1ki/dsh-plugin-subscriptions/main/docs/images/subscriptions.png)
12
+
13
+ 已登录的 provider 会带着实时模型目录进入会话模型选择器:
14
+
15
+ ![模型选择器中的订阅模型](https://raw.githubusercontent.com/V1ki/dsh-plugin-subscriptions/main/docs/images/model-picker.png)
16
+
17
+ `image_generate` 工具生成的图片直接内联显示在对话里:
18
+
19
+ ![image_generate 内联显示生成的图片](https://raw.githubusercontent.com/V1ki/dsh-plugin-subscriptions/main/docs/images/image-generate-inline.png)
20
+
7
21
  ## Provider 一览
8
22
 
9
23
  | 路由 | 订阅 | 模型 |
@@ -21,7 +35,13 @@
21
35
 
22
36
  ## 安装
23
37
 
24
- 本机已有 `dsh` CLI 时:
38
+ 本机已有 `dsh` CLI 时,从 npm 安装(预构建产物,无需构建授权):
39
+
40
+ ```sh
41
+ dsh plugin --profile web add dsh-plugin-subscriptions
42
+ ```
43
+
44
+ 也可以从 GitHub 安装源码:
25
45
 
26
46
  ```sh
27
47
  dsh plugin --profile web add github:V1ki/dsh-plugin-subscriptions
@@ -47,6 +67,7 @@ dsh plugin --profile web add ./dsh-plugin-subscriptions
47
67
  不装进 profile 的 headless 用法(先在 web 界面登录过 —— token 文件是共享的):
48
68
 
49
69
  ```sh
70
+ cp overlay.example.yml overlay.yml # 然后把 name: 改成本检出的 lib/index.js 绝对路径
50
71
  dsh --profile headless --patch <检出目录>/overlay.yml "你的任务"
51
72
  ```
52
73
 
package/lib/auth/rpc.d.ts CHANGED
@@ -5,9 +5,15 @@
5
5
  * outcomes are returned as RpcResult values; handlers never throw.
6
6
  */
7
7
  import type { Context } from '@deepseek-ai/cordis';
8
+ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
8
9
  import { type ProviderId } from './store.js';
9
10
  /** The RPC channel this plugin registers on the host connection. */
10
11
  export declare const SUBSCRIPTIONS_AUTH_CHANNEL = "/subscriptions-auth";
12
+ /** Decoded image bytes returned by the `image` endpoint. */
13
+ export interface ImageBytesResult {
14
+ mediaType: string;
15
+ dataBase64: string;
16
+ }
11
17
  /** Login state of one provider, as rendered by the Settings page. */
12
18
  export interface ProviderStatus {
13
19
  /** Whether a session exists in the store. */
@@ -42,6 +48,14 @@ export interface AuthController {
42
48
  cancel(provider: ProviderId): Promise<void>;
43
49
  /** Delete the stored session. */
44
50
  logout(provider: ProviderId): Promise<void>;
51
+ /**
52
+ * Read one image attachment's bytes for inline display.
53
+ * @param ref - the full durable reference (`readImage` verifies against it).
54
+ * @param signal - caller cancellation from the RPC transport.
55
+ * @returns the media type and base64-encoded bytes.
56
+ * @throws when no attachment service is mounted or the read fails.
57
+ */
58
+ readImage(ref: ImageAttachmentRef, signal: AbortSignal): Promise<ImageBytesResult>;
45
59
  }
46
60
  /**
47
61
  * Register the `/subscriptions-auth` RPC channel when a host connection exists.
package/lib/auth/rpc.js CHANGED
@@ -4,9 +4,12 @@
4
4
  * profile); headless compositions load the plugin without it. All business
5
5
  * outcomes are returned as RpcResult values; handlers never throw.
6
6
  */
7
+ import { AttachmentId } from '@deepseek-ai/dsh-attachment';
7
8
  import { PROVIDER_IDS } from './store.js';
8
9
  /** The RPC channel this plugin registers on the host connection. */
9
10
  export const SUBSCRIPTIONS_AUTH_CHANNEL = '/subscriptions-auth';
11
+ /** Media types the attachment store accepts (ImageMediaType). */
12
+ const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
10
13
  /** Payload carried no usable provider id — an RPC client bug, not a server failure. */
11
14
  class BadRequest extends Error {
12
15
  }
@@ -37,7 +40,39 @@ function readString(payload, field) {
37
40
  }
38
41
  return value;
39
42
  }
40
- async function dispatch(controller, endpoint, payload) {
43
+ /** Validate the `image` endpoint's payload into a full attachment reference. */
44
+ function readImageRef(payload) {
45
+ if (typeof payload !== 'object' || payload === null)
46
+ throw new BadRequest('payload must be an object');
47
+ const record = payload;
48
+ const attachmentId = record.attachmentId;
49
+ if (typeof attachmentId !== 'string' || attachmentId.length === 0) {
50
+ throw new BadRequest('payload.attachmentId must be a non-empty string');
51
+ }
52
+ const mediaType = record.mediaType;
53
+ if (typeof mediaType !== 'string' || !IMAGE_MEDIA_TYPES.includes(mediaType)) {
54
+ throw new BadRequest(`payload.mediaType must be one of ${IMAGE_MEDIA_TYPES.join(', ')}`);
55
+ }
56
+ for (const field of ['bytes', 'width', 'height']) {
57
+ const value = record[field];
58
+ if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
59
+ throw new BadRequest(`payload.${field} must be a positive integer`);
60
+ }
61
+ }
62
+ const name = record.name;
63
+ if (name !== undefined && typeof name !== 'string') {
64
+ throw new BadRequest('payload.name must be a string when present');
65
+ }
66
+ return {
67
+ attachmentId: AttachmentId(attachmentId),
68
+ mediaType: mediaType,
69
+ bytes: record.bytes,
70
+ width: record.width,
71
+ height: record.height,
72
+ ...name === undefined ? {} : { name: name },
73
+ };
74
+ }
75
+ async function dispatch(controller, endpoint, payload, signal) {
41
76
  switch (endpoint) {
42
77
  case 'status': {
43
78
  const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
@@ -56,6 +91,8 @@ async function dispatch(controller, endpoint, payload) {
56
91
  case 'logout':
57
92
  await controller.logout(readProvider(payload));
58
93
  return ok({ ok: true });
94
+ case 'image':
95
+ return ok(await controller.readImage(readImageRef(payload), signal));
59
96
  default:
60
97
  throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
61
98
  }
@@ -71,9 +108,9 @@ export function registerAuthRpc(ctx, controller) {
71
108
  // the service exists instead of probing once at apply time.
72
109
  ctx.inject(['connection'], (ctx) => {
73
110
  const connection = ctx.get('connection');
74
- ctx.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload) => {
111
+ ctx.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
75
112
  try {
76
- return await dispatch(controller, endpoint, payload);
113
+ return await dispatch(controller, endpoint, payload, signal);
77
114
  }
78
115
  catch (error) {
79
116
  return failure(error);
@@ -0,0 +1,48 @@
1
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
+ import type { ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client';
3
+ import type { ImageLoader } from '@deepseek-ai/dsh-client-ui-attachment';
4
+ import type { SubscriptionsKey } from './locales.js';
5
+ /** Mirror of ui-tool's ToolCallOwnerProps (see the module header). */
6
+ interface ToolCallOwnerProps {
7
+ callId: string;
8
+ toolName: string;
9
+ block: ToolCallBlock;
10
+ cwd?: string | undefined;
11
+ openFile: (path: string) => void;
12
+ inspect?: (() => void) | undefined;
13
+ }
14
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
15
+ interface SlotMap {
16
+ /** Mirror of ui-tool's keyed atomic Tool view declaration (see the module header). */
17
+ 'tool.call.toolview': {
18
+ kind: 'keyed';
19
+ scope: 'session';
20
+ owner: ToolCallOwnerProps;
21
+ };
22
+ }
23
+ }
24
+ /** Injected dependencies of {@link ImageGenerateToolview} (slot `inject`). */
25
+ export interface ImageGenerateToolviewInjected {
26
+ /** Session-authorized image URL loader riding the `/subscriptions-auth` channel. */
27
+ load: ImageLoader;
28
+ }
29
+ /**
30
+ * Props delivered by the toolview outlet: the owner share plus the inject
31
+ * face and the framework locale seat, spread flat.
32
+ */
33
+ export type ImageGenerateToolviewProps = Partial<ToolCallOwnerProps> & Partial<ImageGenerateToolviewInjected> & {
34
+ t?: ((key: SubscriptionsKey, params?: Record<string, unknown>) => string) | undefined;
35
+ };
36
+ /**
37
+ * Build the ImageGallery loader over the `image` endpoint.
38
+ * @param rpc - Connection RPC caller.
39
+ * @returns loader resolving an attachment ref to a data URL.
40
+ */
41
+ export declare function createImageLoader(rpc: ConnectionHandle['rpc']): ImageLoader;
42
+ /**
43
+ * The `image_generate` keyed toolview component.
44
+ * @param props - owner share, inject face, and locale seat (spread flat).
45
+ * @returns the call row plus, once settled, the gallery / text / error body.
46
+ */
47
+ export declare function ImageGenerateToolview(props: ImageGenerateToolviewProps): import("react").JSX.Element | null;
48
+ export {};
@@ -0,0 +1,137 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { IconSparkle16 } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { ImageGallery } from '@deepseek-ai/dsh-client-ui-attachment';
4
+ import { en } from './locales.js';
5
+ /** Logical RPC channel served by the node half of this plugin. */
6
+ const SUBSCRIPTIONS_AUTH_CHANNEL = '/subscriptions-auth';
7
+ /** Title prompt truncation budget (characters). */
8
+ const PROMPT_MAX_LENGTH = 60;
9
+ /**
10
+ * Call one `/subscriptions-auth` endpoint and unwrap the business result.
11
+ * @param rpc - Connection RPC caller.
12
+ * @param endpoint - channel-relative endpoint.
13
+ * @param payload - channel-owned request payload.
14
+ * @returns the success value, cast by the caller to the endpoint's shape.
15
+ */
16
+ async function callSubscriptionsAuth(rpc, endpoint, payload) {
17
+ const result = await rpc.call(SUBSCRIPTIONS_AUTH_CHANNEL, endpoint, payload);
18
+ if (!result.ok)
19
+ throw new Error(result.error.message);
20
+ return result.value;
21
+ }
22
+ /**
23
+ * Build the ImageGallery loader over the `image` endpoint.
24
+ * @param rpc - Connection RPC caller.
25
+ * @returns loader resolving an attachment ref to a data URL.
26
+ */
27
+ export function createImageLoader(rpc) {
28
+ // The host validates a full ImageAttachmentRef payload (readImage takes the
29
+ // whole ref), so forward the attachment verbatim.
30
+ return attachment => callSubscriptionsAuth(rpc, 'image', { ...attachment })
31
+ .then(result => `data:${result.mediaType};base64,${result.dataBase64}`);
32
+ }
33
+ /**
34
+ * English-dictionary fallback for a missing locale seat (standalone renders);
35
+ * the framework always supplies the namespace-bound one.
36
+ * @param key - dictionary key.
37
+ * @param params - `{name}` template params.
38
+ * @returns the template with params substituted.
39
+ */
40
+ function fallbackTranslate(key, params) {
41
+ let text = en[key];
42
+ for (const [name, value] of Object.entries(params ?? {})) {
43
+ text = text.replaceAll(`{${name}}`, String(value));
44
+ }
45
+ return text;
46
+ }
47
+ /** Extract the prompt from the call's raw args JSON; falls back to the first string value, then the raw line. */
48
+ function derivePrompt(argsRaw) {
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(argsRaw);
52
+ }
53
+ catch {
54
+ // Non-JSON args (mid-stream truncation): fall back to the raw string below.
55
+ parsed = undefined;
56
+ }
57
+ let prompt;
58
+ if (typeof parsed === 'object' && parsed !== null) {
59
+ const args = parsed;
60
+ if (typeof args.prompt === 'string' && args.prompt !== '')
61
+ prompt = args.prompt;
62
+ else {
63
+ for (const value of Object.values(args)) {
64
+ if (typeof value === 'string' && value !== '') {
65
+ prompt = value;
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ const line = (prompt ?? argsRaw).split('\n', 1)[0] ?? '';
72
+ return line.length > PROMPT_MAX_LENGTH ? `${line.slice(0, PROMPT_MAX_LENGTH)}…` : line;
73
+ }
74
+ /** Flatten a settled result's text blocks (the degraded text-only route and the error line). */
75
+ function resultText(block) {
76
+ if (!('kind' in block))
77
+ return '';
78
+ const parts = [];
79
+ for (const part of block.content) {
80
+ if (part.type === 'text')
81
+ parts.push(part.text);
82
+ }
83
+ if (parts.length === 0 && block.error !== undefined)
84
+ parts.push(`${block.error.name}: ${block.error.code}`);
85
+ return parts.join('\n');
86
+ }
87
+ /** Image attachments of a settled result; empty while running or on the text-only route. */
88
+ function resultImages(block) {
89
+ if (!('kind' in block))
90
+ return [];
91
+ const images = [];
92
+ for (const part of block.content) {
93
+ if (part.type === 'image')
94
+ images.push({ attachment: part.attachment });
95
+ }
96
+ return images;
97
+ }
98
+ const styles = {
99
+ container: { display: 'flex', flexDirection: 'column', gap: 6, padding: '4px 0' },
100
+ row: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 },
101
+ icon: { display: 'inline-flex', flexShrink: 0, color: 'var(--dsw-alias-label-tertiary)' },
102
+ title: {
103
+ fontSize: 13, lineHeight: '20px', color: 'var(--dsw-alias-label-primary)',
104
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
105
+ },
106
+ subtle: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
107
+ output: {
108
+ margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)',
109
+ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere',
110
+ },
111
+ error: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' },
112
+ };
113
+ /**
114
+ * The `image_generate` keyed toolview component.
115
+ * @param props - owner share, inject face, and locale seat (spread flat).
116
+ * @returns the call row plus, once settled, the gallery / text / error body.
117
+ */
118
+ export function ImageGenerateToolview(props) {
119
+ const { block, load } = props;
120
+ const t = props.t ?? fallbackTranslate;
121
+ if (block === undefined)
122
+ return null;
123
+ const settled = 'kind' in block;
124
+ const argsRaw = (settled ? block.call?.argsRaw : block.argsRaw) ?? '';
125
+ const title = `image_generate: ${derivePrompt(argsRaw)}`;
126
+ const images = resultImages(block);
127
+ const text = settled ? resultText(block) : '';
128
+ const labels = {
129
+ image: t('image'),
130
+ open: t('viewImage'),
131
+ openNamed: name => t('viewImageNamed', { name }),
132
+ loading: t('imageLoading'),
133
+ loadFailed: t('imageLoadFailed'),
134
+ lightbox: { dialog: t('imagePreview'), close: t('imageClose') },
135
+ };
136
+ return (_jsxs("div", { style: styles.container, children: [_jsxs("div", { style: styles.row, children: [_jsx("span", { style: styles.icon, children: _jsx(IconSparkle16, { size: 14 }) }), _jsx("span", { style: styles.title, children: title })] }), !settled && _jsx("p", { style: styles.subtle, children: t('generating') }), settled && block.isError && text !== '' && (_jsx("p", { style: styles.error, children: text.split('\n', 1)[0] })), settled && !block.isError && images.length > 0 && load !== undefined && (_jsx(ImageGallery, { images: images, load: load, align: "start", labels: labels })), settled && !block.isError && images.length === 0 && text !== '' && (_jsx("p", { style: styles.output, children: text }))] }));
137
+ }
@@ -9,6 +9,7 @@
9
9
  import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
10
10
  import type { SubscriptionsKey } from './locales.js';
11
11
  export type { SubscriptionsSectionInjected, SubscriptionsSectionProps } from './SubscriptionsSection.js';
12
+ export type { ImageGenerateToolviewInjected, ImageGenerateToolviewProps } from './ImageGenerateToolview.js';
12
13
  export type { SubscriptionsKey } from './locales.js';
13
14
  declare module '@deepseek-ai/dsh-client-ui-slots' {
14
15
  interface LocaleNamespaceMap {
@@ -2,6 +2,7 @@
2
2
  // allowImportingTsExtensions/rewriteRelativeImportExtensions pair; under
3
3
  // nodenext the .js specifier resolves to the .tsx source (see README note).
4
4
  import { SubscriptionsSection } from './SubscriptionsSection.js';
5
+ import { ImageGenerateToolview, createImageLoader } from './ImageGenerateToolview.js';
5
6
  import { en, zh } from './locales.js';
6
7
  /** Dictionary namespace owned by this plugin. */
7
8
  const NS = 'settings.subscriptions';
@@ -32,4 +33,14 @@ export function apply(ctx) {
32
33
  label: () => t('nav'),
33
34
  inject: injected,
34
35
  }, SubscriptionsSection));
36
+ // The image_generate keyed toolview owns how image calls render inline; its
37
+ // gallery bytes ride the same channel through the injected loader. The
38
+ // framework synthesizes the toolview's own `t` seat from `locale: NS`.
39
+ const toolviewInjected = () => ({ load: createImageLoader(connection.rpc) });
40
+ ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
41
+ name: 'tool.call.toolview',
42
+ key: 'image_generate',
43
+ locale: NS,
44
+ inject: toolviewInjected,
45
+ }, ImageGenerateToolview));
35
46
  }
@@ -19,6 +19,14 @@ export declare const en: {
19
19
  manualPlaceholder: string;
20
20
  submit: string;
21
21
  loginMissingUrl: string;
22
+ generating: string;
23
+ image: string;
24
+ viewImage: string;
25
+ viewImageNamed: string;
26
+ imageLoading: string;
27
+ imageLoadFailed: string;
28
+ imagePreview: string;
29
+ imageClose: string;
22
30
  };
23
31
  /** zh strings, one per {@link en} key. */
24
32
  export declare const zh: {
@@ -40,6 +48,14 @@ export declare const zh: {
40
48
  manualPlaceholder: string;
41
49
  submit: string;
42
50
  loginMissingUrl: string;
51
+ generating: string;
52
+ image: string;
53
+ viewImage: string;
54
+ viewImageNamed: string;
55
+ imageLoading: string;
56
+ imageLoadFailed: string;
57
+ imagePreview: string;
58
+ imageClose: string;
43
59
  };
44
60
  /** The Subscriptions namespace key union (en is the key-set source of truth). */
45
61
  export type SubscriptionsKey = keyof typeof en;
@@ -19,6 +19,14 @@ export const en = {
19
19
  manualPlaceholder: 'Paste the callback URL or code',
20
20
  submit: 'Submit',
21
21
  loginMissingUrl: 'login answered without an authorizeUrl',
22
+ generating: 'Generating image…',
23
+ image: 'image',
24
+ viewImage: 'View image',
25
+ viewImageNamed: 'View {name}',
26
+ imageLoading: 'Loading…',
27
+ imageLoadFailed: 'Retry',
28
+ imagePreview: 'Image preview',
29
+ imageClose: 'Close',
22
30
  };
23
31
  /** zh strings, one per {@link en} key. */
24
32
  export const zh = {
@@ -40,4 +48,12 @@ export const zh = {
40
48
  manualPlaceholder: '粘贴回调 URL 或授权码',
41
49
  submit: '提交',
42
50
  loginMissingUrl: 'login 响应缺少 authorizeUrl',
51
+ generating: '正在生成图片…',
52
+ image: '图片',
53
+ viewImage: '查看图片',
54
+ viewImageNamed: '查看 {name}',
55
+ imageLoading: '加载中…',
56
+ imageLoadFailed: '重试',
57
+ imagePreview: '图片预览',
58
+ imageClose: '关闭',
43
59
  };