@talex-touch/utils 1.0.20 → 1.0.22

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
@@ -5,7 +5,7 @@
5
5
  "module": "./index.ts",
6
6
  "license": "MPL-2.0",
7
7
  "private": false,
8
- "version": "1.0.20",
8
+ "version": "1.0.22",
9
9
  "scripts": {
10
10
  "publish": "npm publish --access public"
11
11
  },
package/plugin/index.ts CHANGED
@@ -377,4 +377,5 @@ export interface IManifest {
377
377
  export type { LogLevel, LogItem, LogDataType, IPluginLogger } from './log/types'
378
378
  export * from './sdk/index'
379
379
  export * from './providers'
380
+ export * from './install'
380
381
  export * from './risk'
@@ -0,0 +1,43 @@
1
+ export type PluginInstallTaskStage =
2
+ | 'queued'
3
+ | 'downloading'
4
+ | 'awaiting-confirmation'
5
+ | 'installing'
6
+ | 'completed'
7
+ | 'failed'
8
+ | 'cancelled'
9
+
10
+ export interface PluginInstallProgressEvent {
11
+ taskId: string
12
+ stage: PluginInstallTaskStage
13
+ progress?: number
14
+ message?: string
15
+ error?: string
16
+ /** 安装源标识,例如下载 URL。 */
17
+ source?: string
18
+ /** 官方插件标记。 */
19
+ official?: boolean
20
+ /** 插件唯一标识或名称(由客户端提供)。 */
21
+ pluginId?: string
22
+ pluginName?: string
23
+ /** 队列中的剩余任务数量(包含当前任务)。 */
24
+ remaining?: number
25
+ /** 当前任务在队列中的位置(0 表示正在处理)。 */
26
+ position?: number
27
+ }
28
+
29
+ export interface PluginInstallConfirmRequest {
30
+ taskId: string
31
+ pluginName?: string
32
+ pluginId?: string
33
+ source?: string
34
+ official?: boolean
35
+ }
36
+
37
+ export type PluginInstallConfirmDecision = 'accept' | 'reject'
38
+
39
+ export interface PluginInstallConfirmResponse {
40
+ taskId: string
41
+ decision: PluginInstallConfirmDecision
42
+ reason?: string
43
+ }
@@ -12,6 +12,7 @@ export class PluginLoggerManager {
12
12
  private readonly pluginLogDir: string
13
13
  private readonly sessionLogPath: string
14
14
  private readonly pluginInfoPath: string
15
+ private readonly sessionStart: string
15
16
  private buffer: LogItem[] = []
16
17
  private flushInterval: NodeJS.Timeout
17
18
  private onLogAppend?: (log: LogItem) => void
@@ -25,15 +26,15 @@ export class PluginLoggerManager {
25
26
  constructor(baseDir: string, pluginInfo: ITouchPlugin, onLogAppend?: (log: LogItem) => void) {
26
27
  this.pluginInfo = pluginInfo
27
28
  this.onLogAppend = onLogAppend
28
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
29
+ this.sessionStart = new Date().toISOString()
30
+ const timestamp = this.sessionStart.replace(/[:.]/g, '-')
29
31
  const sessionFolder = `${timestamp}_${pluginInfo.name.replace(/[^a-zA-Z0-9-_]/g, '_')}`
30
32
 
31
33
  this.pluginLogDir = path.resolve(baseDir, 'logs', sessionFolder)
32
34
  this.sessionLogPath = path.resolve(this.pluginLogDir, 'session.log')
33
35
  this.pluginInfoPath = path.resolve(this.pluginLogDir, 'touch-plugin.info')
34
36
 
35
- this.ensureDirectory()
36
- this.createPluginInfoFile()
37
+ this.ensureLogEnvironment(true)
37
38
  this.flushInterval = setInterval(() => this.flush(), 5000)
38
39
  }
39
40
 
@@ -52,8 +53,17 @@ export class PluginLoggerManager {
52
53
  flush(): void {
53
54
  if (this.buffer.length === 0) return
54
55
  const lines = this.buffer.map((item) => structuredStrictStringify(item)).join('\n') + '\n'
55
- fs.appendFileSync(this.sessionLogPath, lines)
56
- this.buffer = []
56
+ try {
57
+ this.ensureLogEnvironment()
58
+ fs.appendFileSync(this.sessionLogPath, lines)
59
+ this.buffer = []
60
+ } catch (error) {
61
+ if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') throw error
62
+ // Directory or file was removed; rebuild and retry once.
63
+ this.ensureLogEnvironment(true)
64
+ fs.appendFileSync(this.sessionLogPath, lines)
65
+ this.buffer = []
66
+ }
57
67
  }
58
68
 
59
69
  /**
@@ -83,12 +93,12 @@ export class PluginLoggerManager {
83
93
  /**
84
94
  * Creates the touch-plugin.info file with plugin information.
85
95
  */
86
- private createPluginInfoFile(): void {
96
+ private writePluginInfoFile(): void {
87
97
  const pluginInfo = {
88
98
  name: this.pluginInfo.name,
89
99
  version: this.pluginInfo.version,
90
100
  description: this.pluginInfo.desc,
91
- sessionStart: new Date().toISOString(),
101
+ sessionStart: this.sessionStart,
92
102
  icon: this.pluginInfo.icon,
93
103
  platforms: this.pluginInfo.platforms,
94
104
  status: this.pluginInfo.status,
@@ -103,11 +113,19 @@ export class PluginLoggerManager {
103
113
  }
104
114
 
105
115
  /**
106
- * Ensures the log directory exists.
116
+ * Ensures the log directory and related files exist.
107
117
  */
108
- private ensureDirectory(): void {
118
+ private ensureLogEnvironment(forceInfo = false): void {
109
119
  if (!fs.existsSync(this.pluginLogDir)) {
110
120
  fs.mkdirSync(this.pluginLogDir, { recursive: true })
111
121
  }
122
+
123
+ if (!fs.existsSync(this.sessionLogPath)) {
124
+ fs.writeFileSync(this.sessionLogPath, '')
125
+ }
126
+
127
+ if (forceInfo || !fs.existsSync(this.pluginInfoPath)) {
128
+ this.writePluginInfoFile()
129
+ }
112
130
  }
113
131
  }
@@ -17,6 +17,8 @@ export interface PluginInstallRequest {
17
17
  hintType?: PluginProviderType
18
18
  /** 可选元数据,在调用链中透传。 */
19
19
  metadata?: Record<string, unknown>
20
+ /** 客户端附加信息,仅用于 UI 状态同步。 */
21
+ clientMetadata?: Record<string, unknown>
20
22
  }
21
23
 
22
24
  export interface PluginProviderContext {
@@ -0,0 +1,48 @@
1
+ import type { ITouchClientChannel } from '@talex-touch/utils/channel';
2
+ import { genChannel } from '../channel';
3
+ import type { IPluginRendererChannel, PluginChannelHandler } from './types';
4
+
5
+ const ensureClientChannel = (): ITouchClientChannel => genChannel();
6
+
7
+ export function createPluginRendererChannel(): IPluginRendererChannel {
8
+ const client = ensureClientChannel();
9
+
10
+ return {
11
+ send(eventName, payload) {
12
+ return client.send(eventName, payload);
13
+ },
14
+
15
+ sendSync(eventName, payload) {
16
+ return client.sendSync(eventName, payload);
17
+ },
18
+
19
+ on(eventName, handler) {
20
+ return client.regChannel(eventName, handler);
21
+ },
22
+
23
+ once(eventName, handler) {
24
+ let dispose: () => void = () => void 0;
25
+ const wrapped: PluginChannelHandler = (event) => {
26
+ dispose();
27
+ handler(event);
28
+ };
29
+
30
+ dispose = client.regChannel(eventName, wrapped);
31
+ return dispose;
32
+ },
33
+
34
+ get raw() {
35
+ return client;
36
+ }
37
+ };
38
+ }
39
+
40
+ let cachedRendererChannel: IPluginRendererChannel | null = null;
41
+
42
+ export function usePluginRendererChannel(): IPluginRendererChannel {
43
+ if (!cachedRendererChannel) {
44
+ cachedRendererChannel = createPluginRendererChannel();
45
+ }
46
+
47
+ return cachedRendererChannel;
48
+ }
@@ -0,0 +1,56 @@
1
+ import type { PluginClipboardHistoryResponse, PluginClipboardItem } from './types'
2
+
3
+ const ensurePluginChannel = () => {
4
+ const channel = (window as any)?.$channel
5
+ if (!channel) {
6
+ throw new Error('[Plugin SDK] Clipboard channel requires plugin renderer context with $channel available.')
7
+ }
8
+ return channel
9
+ }
10
+
11
+ export interface ClipboardHistoryOptions {
12
+ page?: number
13
+ }
14
+
15
+ export interface ClipboardFavoriteOptions {
16
+ id: number
17
+ isFavorite: boolean
18
+ }
19
+
20
+ export interface ClipboardDeleteOptions {
21
+ id: number
22
+ }
23
+
24
+ export function useClipboardHistory() {
25
+ const channel = ensurePluginChannel()
26
+
27
+ return {
28
+ async getLatest(): Promise<PluginClipboardItem | null> {
29
+ return channel.send('clipboard:get-latest')
30
+ },
31
+
32
+ async getHistory(options: ClipboardHistoryOptions = {}): Promise<PluginClipboardHistoryResponse> {
33
+ const { page = 1 } = options
34
+ return channel.send('clipboard:get-history', { page })
35
+ },
36
+
37
+ async setFavorite(options: ClipboardFavoriteOptions): Promise<void> {
38
+ await channel.send('clipboard:set-favorite', options)
39
+ },
40
+
41
+ async deleteItem(options: ClipboardDeleteOptions): Promise<void> {
42
+ await channel.send('clipboard:delete-item', options)
43
+ },
44
+
45
+ async clearHistory(): Promise<void> {
46
+ await channel.send('clipboard:clear-history')
47
+ },
48
+
49
+ onDidChange(callback: (item: PluginClipboardItem) => void): () => void {
50
+ return channel.regChannel('core-box:clipboard-change', ({ data }) => {
51
+ const item = (data && 'item' in data ? data.item : data) as PluginClipboardItem
52
+ callback(item)
53
+ })
54
+ }
55
+ }
56
+ }
@@ -10,6 +10,7 @@ export type BridgeHook<T = any> = (data: T) => void
10
10
 
11
11
  const __hooks: Record<BridgeEvent, Array<BridgeHook>> = {
12
12
  [BridgeEventForCoreBox.CORE_BOX_INPUT_CHANGE]: [],
13
+ [BridgeEventForCoreBox.CORE_BOX_CLIPBOARD_CHANGE]: []
13
14
  }
14
15
 
15
16
  /**
@@ -66,3 +67,5 @@ export const createBridgeHook = <T>(type: BridgeEvent) => (hook: BridgeHook<T>)
66
67
  * @param data The input change data (string).
67
68
  */
68
69
  export const onCoreBoxInputChange = createBridgeHook<{ query: string }>(BridgeEventForCoreBox.CORE_BOX_INPUT_CHANGE)
70
+
71
+ export const onCoreBoxClipboardChange = createBridgeHook<{ item: any }>(BridgeEventForCoreBox.CORE_BOX_CLIPBOARD_CHANGE)
@@ -17,4 +17,6 @@ export * from './window/index'
17
17
  export * from './hooks/index'
18
18
  export * from './service/index'
19
19
 
20
- export * from './storage'
20
+ export * from './channel'
21
+ export * from './clipboard'
22
+ export * from './storage'
@@ -4,6 +4,109 @@
4
4
  * @version 1.0.0
5
5
  */
6
6
 
7
+ import type { ITouchChannel, ITouchClientChannel, StandardChannelData } from '@talex-touch/utils/channel';
8
+
9
+ /**
10
+ * Handler signature for plugin channel events.
11
+ */
12
+ export type PluginChannelHandler = (event: StandardChannelData) => any;
13
+
14
+ /**
15
+ * Bridge exposed to plugin backends for channel-based communication.
16
+ */
17
+ export interface IPluginChannelBridge {
18
+ /**
19
+ * Sends a payload to the main renderer process.
20
+ * @param eventName - Channel event name.
21
+ * @param payload - Optional data payload.
22
+ */
23
+ sendToMain<T = any>(eventName: string, payload?: any): Promise<T>;
24
+
25
+ /**
26
+ * Sends a payload to this plugin's renderer view.
27
+ * @param eventName - Channel event name.
28
+ * @param payload - Optional data payload.
29
+ */
30
+ sendToRenderer<T = any>(eventName: string, payload?: any): Promise<T>;
31
+
32
+ /**
33
+ * Registers a handler for main renderer messages.
34
+ * @param eventName - Channel event name to listen for.
35
+ * @param handler - Handler invoked with the raw channel event.
36
+ * @returns Unsubscribe function.
37
+ */
38
+ onMain(eventName: string, handler: PluginChannelHandler): () => void;
39
+
40
+ /**
41
+ * Registers a handler for renderer-originated messages scoped to this plugin.
42
+ * @param eventName - Channel event name to listen for.
43
+ * @param handler - Handler invoked with the raw channel event.
44
+ * @returns Unsubscribe function.
45
+ */
46
+ onRenderer(eventName: string, handler: PluginChannelHandler): () => void;
47
+
48
+ /**
49
+ * Access to the underlying channel implementation for advanced scenarios.
50
+ */
51
+ readonly raw: ITouchChannel;
52
+ }
53
+
54
+ /**
55
+ * Renderer-side helper for plugin webviews to interact with the bridge channel.
56
+ */
57
+ export interface IPluginRendererChannel {
58
+ /**
59
+ * Sends a message asynchronously and resolves with the reply payload.
60
+ */
61
+ send<T = any>(eventName: string, payload?: any): Promise<T>;
62
+
63
+ /**
64
+ * Sends a message synchronously and returns the reply payload.
65
+ */
66
+ sendSync<T = any>(eventName: string, payload?: any): T;
67
+
68
+ /**
69
+ * Registers a handler for renderer channel events.
70
+ * @returns Unsubscribe function.
71
+ */
72
+ on(eventName: string, handler: PluginChannelHandler): () => void;
73
+
74
+ /**
75
+ * Registers a one-off handler for a renderer channel event.
76
+ * @returns Unsubscribe function (no-op after invocation).
77
+ */
78
+ once(eventName: string, handler: PluginChannelHandler): () => void;
79
+
80
+ /**
81
+ * Provides access to the raw client channel.
82
+ */
83
+ readonly raw: ITouchClientChannel;
84
+ }
85
+
86
+ /**
87
+ * Clipboard history item shared with plugin renderers.
88
+ */
89
+ export interface PluginClipboardItem {
90
+ id?: number
91
+ type: 'text' | 'image' | 'files'
92
+ content: string
93
+ thumbnail?: string | null
94
+ rawContent?: string | null
95
+ sourceApp?: string | null
96
+ timestamp?: string | number | Date | null
97
+ isFavorite?: boolean | null
98
+ }
99
+
100
+ /**
101
+ * Clipboard history pagination response structure.
102
+ */
103
+ export interface PluginClipboardHistoryResponse {
104
+ history: PluginClipboardItem[]
105
+ total: number
106
+ page: number
107
+ pageSize: number
108
+ }
109
+
7
110
  /**
8
111
  * Plugin utilities interface providing core functionality for plugin development
9
112
  *
@@ -28,6 +131,12 @@ export interface IPluginUtils {
28
131
  */
29
132
  clipboard: IClipboardManager;
30
133
 
134
+ /**
135
+ * Channel bridge for communicating with renderer and main processes
136
+ * @see {@link IPluginChannelBridge}
137
+ */
138
+ channel: IPluginChannelBridge;
139
+
31
140
  /**
32
141
  * Search result manager for handling search operations
33
142
  * @see {@link ISearchManager}