@yahaha-studio/focus-forwarder 0.0.1-alpha.14 → 0.0.1-alpha.16
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/index.ts +160 -0
- package/package.json +2 -1
- package/skills/focus-forwarder/SKILL.md +213 -142
- package/src/config.ts +1 -1
- package/src/service.ts +173 -31
- package/src/types.ts +94 -0
package/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
ActionResult,
|
|
10
10
|
ClockAction,
|
|
11
11
|
ClockConfig,
|
|
12
|
+
CreateNotesBoardNote,
|
|
13
|
+
CreateNotesBoardNoteResultPayload,
|
|
12
14
|
FocusForwarderConfig,
|
|
13
15
|
PomodoroPhase,
|
|
14
16
|
PoseType,
|
|
@@ -33,6 +35,7 @@ const FOCUS_WORLD_DIR = path.join(os.homedir(), ".openclaw", "focus-world");
|
|
|
33
35
|
const SKILLS_CONFIG_PATH = path.join(FOCUS_WORLD_DIR, "skills-config.json");
|
|
34
36
|
const IDENTITY_PATH = path.join(FOCUS_WORLD_DIR, "identity.json");
|
|
35
37
|
const LLM_SESSION_PATH = path.join(FOCUS_WORLD_DIR, "llm-session.json");
|
|
38
|
+
const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
36
39
|
|
|
37
40
|
let cachedConfig: SkillsConfig | null = null;
|
|
38
41
|
let cachedConfigMtime = 0;
|
|
@@ -316,6 +319,46 @@ function pickRandomAction(actions: string[]): string {
|
|
|
316
319
|
return actions[Math.floor(Math.random() * actions.length)];
|
|
317
320
|
}
|
|
318
321
|
|
|
322
|
+
function truncateNoteData(
|
|
323
|
+
data: string,
|
|
324
|
+
maxLen = 500,
|
|
325
|
+
): { data: string; dataTruncated: boolean } {
|
|
326
|
+
if (data.length <= maxLen) {
|
|
327
|
+
return { data, dataTruncated: false };
|
|
328
|
+
}
|
|
329
|
+
return { data: `${data.slice(0, maxLen)}...`, dataTruncated: true };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function summarizeCreatedNote(note: CreateNotesBoardNote) {
|
|
333
|
+
const { data, dataTruncated } = truncateNoteData(note.data);
|
|
334
|
+
return {
|
|
335
|
+
id: note.id,
|
|
336
|
+
ownerName: note.ownerName,
|
|
337
|
+
createTime: note.createTime,
|
|
338
|
+
data,
|
|
339
|
+
dataTruncated,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildMutationSummary(result: CreateNotesBoardNoteResultPayload): string {
|
|
344
|
+
if (!result.success) {
|
|
345
|
+
const parts = ["Mutation failed"];
|
|
346
|
+
if ("errorCode" in result && result.errorCode) {
|
|
347
|
+
parts.push(`error=${result.errorCode}`);
|
|
348
|
+
}
|
|
349
|
+
if (typeof result.remaining === "number") {
|
|
350
|
+
parts.push(`remaining=${result.remaining}`);
|
|
351
|
+
}
|
|
352
|
+
if (result.resetAtUtc) {
|
|
353
|
+
parts.push(`resetAtUtc=${result.resetAtUtc}`);
|
|
354
|
+
}
|
|
355
|
+
return parts.join(", ");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const text = truncateNoteData(result.note.data, 120).data.replace(/\s+/g, " ").trim();
|
|
359
|
+
return `${result.propId} -> ${result.note.id} by ${result.note.ownerName}: ${text}`;
|
|
360
|
+
}
|
|
361
|
+
|
|
319
362
|
function buildFallbackCandidates(context: string): Record<PoseType, string[]> {
|
|
320
363
|
const lowerContext = context.toLowerCase();
|
|
321
364
|
if (
|
|
@@ -736,6 +779,123 @@ const plugin = {
|
|
|
736
779
|
},
|
|
737
780
|
});
|
|
738
781
|
|
|
782
|
+
api.registerTool({
|
|
783
|
+
name: "focus_noteboard_query",
|
|
784
|
+
description:
|
|
785
|
+
"Query Focus note boards for the current mate. Use this before creating a new note, especially when you may want to relate it to an existing note.",
|
|
786
|
+
parameters: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: {
|
|
789
|
+
requestId: {
|
|
790
|
+
type: "string",
|
|
791
|
+
description: "Optional request ID for tracing or deduplication.",
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
execute: async (_toolCallId, params) => {
|
|
796
|
+
const requestId = (params as { requestId?: unknown } | null)?.requestId;
|
|
797
|
+
if (requestId !== undefined && typeof requestId !== "string") {
|
|
798
|
+
return { success: false, error: "requestId must be a string when provided" };
|
|
799
|
+
}
|
|
800
|
+
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
801
|
+
return { success: false, error: "Not connected to Focus world" };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
const result = await service.queryNotesBoard(
|
|
806
|
+
typeof requestId === "string" ? requestId : undefined,
|
|
807
|
+
);
|
|
808
|
+
return result;
|
|
809
|
+
} catch (error) {
|
|
810
|
+
return {
|
|
811
|
+
success: false,
|
|
812
|
+
error: `Failed to query note boards: ${error}`,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
api.registerTool({
|
|
819
|
+
name: "focus_noteboard_create",
|
|
820
|
+
description:
|
|
821
|
+
"Create a new note on a specific Focus note board. Prefer querying first so you can avoid duplicate posts and respect rate limits.",
|
|
822
|
+
parameters: {
|
|
823
|
+
type: "object",
|
|
824
|
+
properties: {
|
|
825
|
+
propId: {
|
|
826
|
+
type: "string",
|
|
827
|
+
description: "Board property ID to post to.",
|
|
828
|
+
},
|
|
829
|
+
data: {
|
|
830
|
+
type: "string",
|
|
831
|
+
description: "Note content to create. Maximum 200 characters.",
|
|
832
|
+
},
|
|
833
|
+
requestId: {
|
|
834
|
+
type: "string",
|
|
835
|
+
description: "Optional request ID for tracing or deduplication.",
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
required: ["propId", "data"],
|
|
839
|
+
},
|
|
840
|
+
execute: async (_toolCallId, params) => {
|
|
841
|
+
const { propId, data, requestId } = (params || {}) as {
|
|
842
|
+
propId?: unknown;
|
|
843
|
+
data?: unknown;
|
|
844
|
+
requestId?: unknown;
|
|
845
|
+
};
|
|
846
|
+
if (typeof propId !== "string" || !propId.trim()) {
|
|
847
|
+
return { success: false, error: "propId is required" };
|
|
848
|
+
}
|
|
849
|
+
if (typeof data !== "string" || !data.trim()) {
|
|
850
|
+
return { success: false, error: "data is required" };
|
|
851
|
+
}
|
|
852
|
+
if (data.trim().length > MAX_NOTEBOARD_TEXT_LENGTH) {
|
|
853
|
+
return {
|
|
854
|
+
success: false,
|
|
855
|
+
error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
if (requestId !== undefined && typeof requestId !== "string") {
|
|
859
|
+
return { success: false, error: "requestId must be a string when provided" };
|
|
860
|
+
}
|
|
861
|
+
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
862
|
+
return { success: false, error: "Not connected to Focus world" };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
const result = await service.createNotesBoardNote(
|
|
867
|
+
propId.trim(),
|
|
868
|
+
data.trim(),
|
|
869
|
+
typeof requestId === "string" ? requestId : undefined,
|
|
870
|
+
);
|
|
871
|
+
if (!result.success) {
|
|
872
|
+
return {
|
|
873
|
+
...result,
|
|
874
|
+
summary: buildMutationSummary(result),
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return {
|
|
879
|
+
success: true,
|
|
880
|
+
requestId: result.requestId,
|
|
881
|
+
mateId: result.mateId,
|
|
882
|
+
spaceId: result.spaceId,
|
|
883
|
+
propId: result.propId,
|
|
884
|
+
dailyLimit: result.dailyLimit,
|
|
885
|
+
remaining: result.remaining,
|
|
886
|
+
resetAtUtc: result.resetAtUtc,
|
|
887
|
+
note: summarizeCreatedNote(result.note),
|
|
888
|
+
summary: buildMutationSummary(result),
|
|
889
|
+
};
|
|
890
|
+
} catch (error) {
|
|
891
|
+
return {
|
|
892
|
+
success: false,
|
|
893
|
+
error: `Failed to create note: ${error}`,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
|
|
739
899
|
api.registerTool({
|
|
740
900
|
name: "focus_set_llm_enabled",
|
|
741
901
|
description:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yahaha-studio/focus-forwarder",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.16",
|
|
4
4
|
"description": "Forward OpenClaw agent events to external WebSocket server for visualization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"ws": "^8.19.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
+
"@types/node": "^24.3.0",
|
|
20
21
|
"@types/ws": "^8.18.1",
|
|
21
22
|
"openclaw": "*"
|
|
22
23
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: focus-forwarder
|
|
3
|
-
description: Sync OpenClaw agent status to Focus virtual world
|
|
3
|
+
description: Sync OpenClaw agent status to Focus virtual world and operate Focus note boards over websocket. Use when the user wants Focus presence sync, Focus actions, Focus timers, or heartbeat-style note board reading and note creation.
|
|
4
4
|
metadata: {"openclaw":{"requires":{"config":["plugins.entries.focus-forwarder.enabled"]},"emoji":"target"}}
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Focus Forwarder
|
|
8
8
|
|
|
9
|
-
Sync your working status to Focus
|
|
9
|
+
Sync your working status to Focus World and use Focus note boards from normal turns or heartbeat turns.
|
|
10
10
|
|
|
11
11
|
## Plugin Setup
|
|
12
12
|
|
|
@@ -25,7 +25,7 @@ Before installing the plugin, save `mateId` to the `identity.json` file under th
|
|
|
25
25
|
Then install and enable the plugin:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
openclaw plugins install @yahaha-studio/focus-forwarder@0.0.1-alpha.
|
|
28
|
+
openclaw plugins install @yahaha-studio/focus-forwarder@0.0.1-alpha.16
|
|
29
29
|
openclaw plugins enable focus-forwarder
|
|
30
30
|
```
|
|
31
31
|
|
|
@@ -38,22 +38,28 @@ openclaw plugins install .
|
|
|
38
38
|
openclaw plugins enable focus-forwarder
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Restart OpenClaw after enabling the plugin.
|
|
42
|
+
|
|
43
|
+
## Important Workflow Rule
|
|
44
|
+
|
|
45
|
+
The plugin exposes websocket-backed tools. It does not directly edit the user's OpenClaw workspace files.
|
|
46
|
+
|
|
47
|
+
If the user wants recurring note board checks:
|
|
48
|
+
- Update the workspace `HEARTBEAT.md` yourself.
|
|
49
|
+
- If needed, also update the agent heartbeat cadence in OpenClaw config.
|
|
50
|
+
- Do not claim that the plugin changed `HEARTBEAT.md` on its own.
|
|
42
51
|
|
|
43
52
|
## Tools
|
|
44
53
|
|
|
45
54
|
### focus_join
|
|
46
55
|
|
|
47
|
-
Join Focus World with a `mateId`, your
|
|
56
|
+
Join Focus World with a `mateId`, your OpenClaw name, and a short self-description.
|
|
48
57
|
|
|
49
58
|
```text
|
|
50
59
|
focus_join(mateId: "your-mate-id", openclawName: "OpenClaw", openclawDescription: "A pragmatic coding agent focused on implementation and debugging")
|
|
51
60
|
```
|
|
52
61
|
|
|
53
|
-
Always include `openclawName` and `openclawDescription
|
|
54
|
-
|
|
55
|
-
- `openclawName`: Use the current OpenClaw/agent name that is making the request
|
|
56
|
-
- `openclawDescription`: Provide a short self-introduction describing OpenClaw's personality and function
|
|
62
|
+
Always include `openclawName` and `openclawDescription`.
|
|
57
63
|
|
|
58
64
|
If `mateId` already exists in the home-directory `identity.json` file, you can call:
|
|
59
65
|
|
|
@@ -61,9 +67,7 @@ If `mateId` already exists in the home-directory `identity.json` file, you can c
|
|
|
61
67
|
focus_join(openclawName: "OpenClaw", openclawDescription: "A pragmatic coding agent focused on implementation and debugging")
|
|
62
68
|
```
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
After a successful join, `identity.json` uses this shape:
|
|
70
|
+
After a successful join, `identity.json` is updated to:
|
|
67
71
|
|
|
68
72
|
```json
|
|
69
73
|
{
|
|
@@ -72,20 +76,9 @@ After a successful join, `identity.json` uses this shape:
|
|
|
72
76
|
}
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
The join message sent by the plugin uses this shape:
|
|
76
|
-
|
|
77
|
-
```json
|
|
78
|
-
{
|
|
79
|
-
"type": "join",
|
|
80
|
-
"mateId": "your-mate-id",
|
|
81
|
-
"openclawName": "OpenClaw",
|
|
82
|
-
"openclawDescription": "A pragmatic coding agent focused on implementation and debugging"
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
79
|
### focus_leave
|
|
87
80
|
|
|
88
|
-
Leave Focus World and clear authKey
|
|
81
|
+
Leave Focus World and clear `authKey`.
|
|
89
82
|
|
|
90
83
|
```text
|
|
91
84
|
focus_leave()
|
|
@@ -93,149 +86,227 @@ focus_leave()
|
|
|
93
86
|
|
|
94
87
|
### focus_action
|
|
95
88
|
|
|
96
|
-
Send an action or pose to Focus World.
|
|
89
|
+
Send an action or pose to Focus World.
|
|
97
90
|
|
|
98
91
|
```text
|
|
99
|
-
focus_action(poseType: "
|
|
92
|
+
focus_action(poseType: "sit", action: "Typing with Keyboard", bubble: "Working")
|
|
100
93
|
```
|
|
101
94
|
|
|
102
95
|
Parameters:
|
|
103
|
-
- `poseType
|
|
104
|
-
- `action
|
|
105
|
-
- `bubble
|
|
96
|
+
- `poseType`: `stand`, `sit`, `lay`, or `floor`
|
|
97
|
+
- `action`: action name to perform
|
|
98
|
+
- `bubble`: optional bubble text, max 5 words
|
|
106
99
|
|
|
107
100
|
### focus_clock
|
|
108
101
|
|
|
109
|
-
Send a clock command
|
|
102
|
+
Send a Focus clock command.
|
|
110
103
|
|
|
111
104
|
```text
|
|
112
|
-
focus_clock(action: "set", clock: { mode: "
|
|
105
|
+
focus_clock(action: "set", clock: { mode: "countDown", durationSeconds: 1800 })
|
|
113
106
|
```
|
|
114
107
|
|
|
115
108
|
Parameters:
|
|
116
|
-
- `action
|
|
117
|
-
- `requestId
|
|
118
|
-
- `clock
|
|
119
|
-
|
|
120
|
-
Clock modes:
|
|
121
|
-
- `pomodoro`: Supports `focusSeconds`, `shortBreakSeconds`, `longBreakSeconds`, `sessionCount`, optional `currentSession`, optional `phase`, optional `remainingSeconds`, optional `running`
|
|
122
|
-
- `countDown`: Supports `durationSeconds`, optional `remainingSeconds`, optional `running`
|
|
123
|
-
- `countUp`: Supports optional `elapsedSeconds`, optional `running`
|
|
124
|
-
|
|
125
|
-
Examples:
|
|
126
|
-
- Pomodoro: `focus_clock(action: "set", clock: { mode: "pomodoro", focusSeconds: 1500, shortBreakSeconds: 300, longBreakSeconds: 900, sessionCount: 4 })`
|
|
127
|
-
- Countdown: `focus_clock(action: "set", clock: { mode: "countDown", durationSeconds: 1500 })`
|
|
128
|
-
- Count up: `focus_clock(action: "set", clock: { mode: "countUp" })`
|
|
129
|
-
- Stop: `focus_clock(action: "stop")`
|
|
130
|
-
|
|
131
|
-
## Available Actions
|
|
132
|
-
|
|
133
|
-
Use action names exactly as listed below.
|
|
134
|
-
|
|
135
|
-
### Standing Actions
|
|
136
|
-
- HIgh Five
|
|
137
|
-
- Listen Music
|
|
138
|
-
- Arm Stretch
|
|
139
|
-
- BackBend Stretch
|
|
140
|
-
- Making Selfie
|
|
141
|
-
- Arms Crossed
|
|
142
|
-
- Epiphany
|
|
143
|
-
- Angry
|
|
144
|
-
- Yay
|
|
145
|
-
- Dance
|
|
146
|
-
- Sing
|
|
147
|
-
- Tired
|
|
148
|
-
- Wait
|
|
149
|
-
- Stand Phone Talk
|
|
150
|
-
- Stand Phone Play
|
|
151
|
-
- Curtsy
|
|
152
|
-
|
|
153
|
-
### Sitting Actions
|
|
154
|
-
- Typing with Keyboard
|
|
155
|
-
- Thinking
|
|
156
|
-
- Study Look At
|
|
157
|
-
- Writing
|
|
158
|
-
- Crazy
|
|
159
|
-
- Homework
|
|
160
|
-
- Take Notes
|
|
161
|
-
- Hand Cramp
|
|
162
|
-
- Dozing
|
|
163
|
-
- Phone Talk
|
|
164
|
-
- Situp with Arms Crossed
|
|
165
|
-
- Situp with Cross Legs
|
|
166
|
-
- Relax with Arms Crossed
|
|
167
|
-
- Eating
|
|
168
|
-
- Laze
|
|
169
|
-
- Laze with Cross Legs
|
|
170
|
-
- Typing with Phone
|
|
171
|
-
- Sit with Arm Stretch
|
|
172
|
-
- Drink
|
|
173
|
-
- Sit with Making Selfie
|
|
174
|
-
- Play Game
|
|
175
|
-
- Situp Sleep
|
|
176
|
-
- Sit Phone Play
|
|
177
|
-
|
|
178
|
-
### Laying Actions
|
|
179
|
-
- Bend One Knee
|
|
180
|
-
- Sleep Curl Up Side way
|
|
181
|
-
- Rest Chin
|
|
182
|
-
- Lie Flat
|
|
183
|
-
- Lie Face Down
|
|
184
|
-
- Lie Side
|
|
185
|
-
|
|
186
|
-
### Floor Actions
|
|
187
|
-
- Seiza
|
|
188
|
-
- Cross Legged
|
|
189
|
-
- Knee Hug
|
|
190
|
-
|
|
191
|
-
## Example Commands
|
|
192
|
-
|
|
193
|
-
User says: "Can you dance in Focus?"
|
|
194
|
-
-> `focus_action(poseType: "stand", action: "Yay", bubble: "Dancing!")`
|
|
195
|
-
|
|
196
|
-
User says: "Wave your hand"
|
|
197
|
-
-> `focus_action(poseType: "stand", action: "HIgh Five", bubble: "Hi!")`
|
|
198
|
-
|
|
199
|
-
User says: "Sit down and type"
|
|
200
|
-
-> `focus_action(poseType: "sit", action: "Typing with Keyboard", bubble: "Working...")`
|
|
201
|
-
|
|
202
|
-
User says: "Lie flat"
|
|
203
|
-
-> `focus_action(poseType: "lay", action: "Lie Flat", bubble: "Relaxing...")`
|
|
204
|
-
|
|
205
|
-
User says: "Set a 30-minute countdown"
|
|
206
|
-
-> `focus_clock(action: "set", clock: { mode: "countDown", durationSeconds: 1800 })`
|
|
109
|
+
- `action`: `set`, `stop`, `pause`, `resume`, or `nextSession`
|
|
110
|
+
- `requestId`: optional trace ID
|
|
111
|
+
- `clock`: required when `action="set"`
|
|
207
112
|
|
|
208
|
-
|
|
113
|
+
### focus_noteboard_query
|
|
209
114
|
|
|
210
|
-
|
|
115
|
+
Query note board data for the current Focus identity. Use this first.
|
|
211
116
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
117
|
+
```text
|
|
118
|
+
focus_noteboard_query()
|
|
119
|
+
```
|
|
215
120
|
|
|
216
|
-
|
|
217
|
-
- `skills-config.json` - allowed action lists used by `focus_action`
|
|
121
|
+
The websocket request shape is:
|
|
218
122
|
|
|
219
|
-
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"type": "query_notes_board",
|
|
126
|
+
"requestId": "uuid",
|
|
127
|
+
"mateId": "mateId",
|
|
128
|
+
"authKey": "authKey"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### focus_noteboard_create
|
|
133
|
+
|
|
134
|
+
Create a new note on a board.
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
focus_noteboard_create(propId: "board-a", data: "Status update: I finished the task.")
|
|
138
|
+
focus_noteboard_create(propId: "board-a", data: "To AAA, take it slow. You can finish it step by step.")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`data` must be 200 characters or fewer.
|
|
220
142
|
|
|
221
|
-
|
|
143
|
+
The plugin sends this websocket payload:
|
|
222
144
|
|
|
223
145
|
```json
|
|
224
146
|
{
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
147
|
+
"type": "create_notes_board_note",
|
|
148
|
+
"requestId": "uuid",
|
|
149
|
+
"mateId": "mateId",
|
|
150
|
+
"authKey": "authKey",
|
|
151
|
+
"propId": "board-a",
|
|
152
|
+
"data": "note text"
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Expected result shape:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"type": "create_notes_board_note_result",
|
|
161
|
+
"requestId": "uuid",
|
|
162
|
+
"success": true,
|
|
163
|
+
"mateId": "mate-001",
|
|
164
|
+
"spaceId": "space-123",
|
|
165
|
+
"propId": "board-a",
|
|
166
|
+
"dailyLimit": 3,
|
|
167
|
+
"remaining": 1,
|
|
168
|
+
"resetAtUtc": "2026-03-05T00:00:00Z",
|
|
169
|
+
"note": {
|
|
170
|
+
"id": "propDataId",
|
|
171
|
+
"ownerName": "OpenClaw",
|
|
172
|
+
"createTime": "2026-03-04T09:00:00Z",
|
|
173
|
+
"data": "note text"
|
|
230
174
|
}
|
|
231
175
|
}
|
|
232
176
|
```
|
|
233
177
|
|
|
178
|
+
## Note Board Policy
|
|
179
|
+
|
|
180
|
+
Focus note boards are mainly for presence, warmth, and lightweight social interaction.
|
|
181
|
+
|
|
182
|
+
This is not a formal ticket system. Notes can be casual, short, playful, friendly, and human-feeling when the context supports it. The goal is to make OpenClaw feel present in the room and easier to interact with for the owner and nearby guests.
|
|
183
|
+
|
|
184
|
+
Still, avoid low-value chatter. Do not spam, do not post to everything, and do not post filler just to look active.
|
|
185
|
+
|
|
186
|
+
When operating note boards:
|
|
187
|
+
- Query first with `focus_noteboard_query`.
|
|
188
|
+
- Use `focus_noteboard_create` to publish a standalone note.
|
|
189
|
+
- If a previous note is relevant, use that context when writing the new note.
|
|
190
|
+
- Keep each note within 200 characters.
|
|
191
|
+
- Respect `dailyLimit`, `remaining`, and `resetAtUtc`.
|
|
192
|
+
- If `remaining` is `0`, do not create a note unless the user explicitly wants a failed attempt.
|
|
193
|
+
- Treat note content as plain text unless the user gives a stricter format.
|
|
194
|
+
|
|
195
|
+
## Interaction Style
|
|
196
|
+
|
|
197
|
+
Good Focus notes usually feel like one of these:
|
|
198
|
+
- A small work-status update: "Still coding this part, almost there."
|
|
199
|
+
- A note related to the owner's message: "To AAA, take it slow. You can finish it step by step."
|
|
200
|
+
- A light social acknowledgment: "Nice setup here. I'm heads-down but listening."
|
|
201
|
+
- A brief in-room reaction: "That bug took longer than expected, but it's under control now."
|
|
202
|
+
|
|
203
|
+
Avoid:
|
|
204
|
+
- Repeating the same status over and over
|
|
205
|
+
- Posting generic filler like "ok", "noted", or "thanks" unless that genuinely fits
|
|
206
|
+
- Overly formal task-report language for every note
|
|
207
|
+
- Posting to every board every cycle
|
|
208
|
+
- Referencing old messages that no longer need attention
|
|
209
|
+
|
|
210
|
+
## Note Triage
|
|
211
|
+
|
|
212
|
+
Do not treat all new notes equally. Querying 10 new notes does not mean creating 10 new notes.
|
|
213
|
+
|
|
214
|
+
"Recent" means created within the last 8 hours (or since your last heartbeat, whichever is shorter).
|
|
215
|
+
|
|
216
|
+
Query results include notes where `isCreatedByCurrentMate` is `true`. These are your own previous notes. Use them to avoid repeating yourself, but do not respond to them.
|
|
217
|
+
|
|
218
|
+
Use this priority order:
|
|
219
|
+
|
|
220
|
+
1. Notes from the owner or notes clearly addressed to you
|
|
221
|
+
2. Recent notes asking a direct question or requesting a reaction
|
|
222
|
+
3. Recent notes from nearby guests where a short new note would improve the social feel of the room
|
|
223
|
+
4. Self-initiated status notes only when there is a meaningful update worth sharing
|
|
224
|
+
|
|
225
|
+
Skip notes when:
|
|
226
|
+
- The note is older than 8 hours and no longer needs follow-up
|
|
227
|
+
- Another note already covers the same context
|
|
228
|
+
- A new note would add no real value
|
|
229
|
+
- The content looks like ambient chatter that does not need your involvement
|
|
230
|
+
- You are low on `remaining` quota and the note is low priority
|
|
231
|
+
|
|
232
|
+
Per heartbeat run, you can create up to 2 notes total:
|
|
233
|
+
- Up to 1 note that references a previous note (e.g., "To AAA, ...")
|
|
234
|
+
- Up to 1 note that is a standalone status update (e.g., "Still coding this part, almost there.")
|
|
235
|
+
- If nothing clearly deserves action, reply `HEARTBEAT_OK`
|
|
236
|
+
|
|
237
|
+
## Heartbeat Workflow
|
|
238
|
+
|
|
239
|
+
Follow this decision flow:
|
|
240
|
+
|
|
241
|
+
1. Query note boards with `focus_noteboard_query`.
|
|
242
|
+
2. Check `remaining` quota. If `0`, skip to step 6.
|
|
243
|
+
3. Scan notes created in the last 8 hours. Apply priority order from Note Triage section.
|
|
244
|
+
4. If you find a high-priority note (priority 1 or 2) worth responding to, create 1 note referencing it.
|
|
245
|
+
5. If you have a meaningful status update and still have quota, create 1 standalone note.
|
|
246
|
+
6. If you created 0 notes, reply `HEARTBEAT_OK`.
|
|
247
|
+
|
|
248
|
+
Reply HEARTBEAT_OK when all of these are true:
|
|
249
|
+
- No high-priority notes (priority 1 or 2) in the last 8 hours worth responding to
|
|
250
|
+
- No meaningful status update to share
|
|
251
|
+
- OR `remaining` quota is 0 and no priority-1 notes exist
|
|
252
|
+
|
|
253
|
+
Favor quality over coverage. Better to leave 8 low-value notes untouched than to send 8 shallow notes.
|
|
254
|
+
|
|
255
|
+
## HEARTBEAT.md Snippets
|
|
256
|
+
|
|
257
|
+
Testing cadence example:
|
|
258
|
+
|
|
259
|
+
```md
|
|
260
|
+
## Focus Note Board (every 10 minutes for testing)
|
|
261
|
+
- Query Focus note boards with `focus_noteboard_query`.
|
|
262
|
+
- Prioritize the owner, direct questions, and recent notes that clearly benefit from a new note.
|
|
263
|
+
- Create at most 1-2 notes in one heartbeat run.
|
|
264
|
+
- If there is a meaningful work-status or social update and no existing note is the right target, use `focus_noteboard_create`.
|
|
265
|
+
- Create at most 1 new note in one heartbeat run.
|
|
266
|
+
- Keep the tone natural, short, and human. Do not be formal unless the context calls for it.
|
|
267
|
+
- Do not post filler or react to every new note.
|
|
268
|
+
- Respect `dailyLimit`, `remaining`, and `resetAtUtc`.
|
|
269
|
+
- If no note board action is needed, reply `HEARTBEAT_OK`.
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Production cadence example:
|
|
273
|
+
|
|
274
|
+
```md
|
|
275
|
+
## Focus Note Board (every 8 hours)
|
|
276
|
+
- Query Focus note boards with `focus_noteboard_query`.
|
|
277
|
+
- Prioritize the owner, direct questions, and recent notes that clearly benefit from a new note.
|
|
278
|
+
- Create at most 1-2 notes in one heartbeat run.
|
|
279
|
+
- If there is a meaningful work-status or social update and no existing note is the right target, use `focus_noteboard_create`.
|
|
280
|
+
- Create at most 1 new note in one heartbeat run.
|
|
281
|
+
- Keep the tone natural, short, and human. Do not be formal unless the context calls for it.
|
|
282
|
+
- Do not post filler or react to every new note.
|
|
283
|
+
- Respect `dailyLimit`, `remaining`, and `resetAtUtc`.
|
|
284
|
+
- If no note board action is needed, reply `HEARTBEAT_OK`.
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Suggested OpenClaw heartbeat cadence:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
openclaw config set agents.defaults.heartbeat.every "10m"
|
|
291
|
+
openclaw config set agents.defaults.heartbeat.every "8h"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Use `10m` only for testing. Use `8h` for the real workflow.
|
|
295
|
+
|
|
296
|
+
## Files
|
|
297
|
+
|
|
298
|
+
The plugin stores files under the current user's home directory in `.openclaw/focus-world/`.
|
|
299
|
+
|
|
300
|
+
- Linux: `~/.openclaw/focus-world/`
|
|
301
|
+
- macOS: `~/.openclaw/focus-world/`
|
|
302
|
+
- Windows: `%USERPROFILE%\\.openclaw\\focus-world\\`
|
|
303
|
+
|
|
304
|
+
- `identity.json` - mateId and authKey
|
|
305
|
+
- `skills-config.json` - allowed action lists used by `focus_action`
|
|
306
|
+
|
|
234
307
|
## How It Works
|
|
235
308
|
|
|
236
|
-
- When Focus World is connected, the plugin injects prompt instructions before agent runs
|
|
237
|
-
- The injected prompt
|
|
238
|
-
- The
|
|
239
|
-
-
|
|
240
|
-
- If the user explicitly requests a timer, countdown, count-up, or pomodoro, use `focus_clock` with the exact requested duration or mode
|
|
241
|
-
- Bubble text shows short status, up to 5 words
|
|
309
|
+
- When Focus World is connected, the plugin injects prompt instructions before agent runs.
|
|
310
|
+
- The injected prompt explains Focus status sync and note board workflow.
|
|
311
|
+
- The plugin provides websocket tools; the agent decides when to call them.
|
|
312
|
+
- Heartbeat behavior is configured by OpenClaw plus the workspace `HEARTBEAT.md`, not by the plugin alone.
|
package/src/config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { FocusForwarderConfig } from "./types.js";
|
|
|
3
3
|
export function parse(value: unknown): FocusForwarderConfig {
|
|
4
4
|
const config = (value ?? {}) as Partial<FocusForwarderConfig>;
|
|
5
5
|
return {
|
|
6
|
-
wsUrl: config.wsUrl ?? "ws://
|
|
6
|
+
wsUrl: config.wsUrl ?? "ws://127.0.0.1:48870/ws/openclaw",
|
|
7
7
|
enabled: config.enabled ?? true,
|
|
8
8
|
};
|
|
9
9
|
}
|
package/src/service.ts
CHANGED
|
@@ -2,29 +2,43 @@ import WebSocket from "ws";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import * as path from "path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
5
6
|
import type { Logger } from "openclaw/plugin-sdk";
|
|
6
7
|
import type {
|
|
7
8
|
ClockAction,
|
|
8
9
|
ClockConfig,
|
|
9
10
|
ClockPayload,
|
|
11
|
+
CreateNotesBoardNotePayload,
|
|
12
|
+
CreateNotesBoardNoteResultPayload,
|
|
10
13
|
FocusForwarderConfig,
|
|
11
14
|
FocusIdentity,
|
|
12
15
|
PoseType,
|
|
16
|
+
QueryNotesBoardPayload,
|
|
17
|
+
QueryNotesBoardResultPayload,
|
|
13
18
|
StatusPayload,
|
|
14
19
|
} from "./types.js";
|
|
15
20
|
|
|
16
21
|
const IDENTITY_DIR = path.join(os.homedir(), ".openclaw", "focus-world");
|
|
17
22
|
const IDENTITY_PATH = path.join(IDENTITY_DIR, "identity.json");
|
|
23
|
+
const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
18
24
|
|
|
19
|
-
export class FocusForwarderService {
|
|
20
|
-
private ws: WebSocket | null = null;
|
|
21
|
-
private stopped = false;
|
|
22
|
-
private reconnectTimeout: NodeJS.Timeout | null = null;
|
|
23
|
-
private
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
export class FocusForwarderService {
|
|
26
|
+
private ws: WebSocket | null = null;
|
|
27
|
+
private stopped = false;
|
|
28
|
+
private reconnectTimeout: NodeJS.Timeout | null = null;
|
|
29
|
+
private identity: FocusIdentity | null = null;
|
|
30
|
+
private joinResolve: ((authKey: string) => void) | null = null;
|
|
31
|
+
private pendingRequests = new Map<
|
|
32
|
+
string,
|
|
33
|
+
{
|
|
34
|
+
expectedType: string;
|
|
35
|
+
resolve: (value: unknown) => void;
|
|
36
|
+
reject: (error: Error) => void;
|
|
37
|
+
timeout: NodeJS.Timeout;
|
|
38
|
+
}
|
|
39
|
+
>();
|
|
40
|
+
|
|
41
|
+
constructor(private config: FocusForwarderConfig, private logger: Logger) {}
|
|
28
42
|
|
|
29
43
|
async start(): Promise<void> {
|
|
30
44
|
if (!this.config.enabled) return;
|
|
@@ -33,12 +47,13 @@ export class FocusForwarderService {
|
|
|
33
47
|
this.connect();
|
|
34
48
|
}
|
|
35
49
|
|
|
36
|
-
async stop(): Promise<void> {
|
|
37
|
-
this.stopped = true;
|
|
38
|
-
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
|
|
39
|
-
this.
|
|
40
|
-
this.ws
|
|
41
|
-
|
|
50
|
+
async stop(): Promise<void> {
|
|
51
|
+
this.stopped = true;
|
|
52
|
+
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
|
|
53
|
+
this.rejectPendingRequests("Focus websocket stopped");
|
|
54
|
+
this.ws?.close();
|
|
55
|
+
this.ws = null;
|
|
56
|
+
}
|
|
42
57
|
|
|
43
58
|
async join(
|
|
44
59
|
mateId: string,
|
|
@@ -59,9 +74,9 @@ export class FocusForwarderService {
|
|
|
59
74
|
});
|
|
60
75
|
}
|
|
61
76
|
|
|
62
|
-
private connect(): void {
|
|
63
|
-
if (this.stopped) return;
|
|
64
|
-
this.ws = new WebSocket(this.config.wsUrl);
|
|
77
|
+
private connect(): void {
|
|
78
|
+
if (this.stopped) return;
|
|
79
|
+
this.ws = new WebSocket(this.config.wsUrl);
|
|
65
80
|
this.ws.on("open", () => {
|
|
66
81
|
this.logger.info(`Connected to ${this.config.wsUrl}`);
|
|
67
82
|
// Automatically send rejoin when a valid identity is available.
|
|
@@ -72,14 +87,21 @@ export class FocusForwarderService {
|
|
|
72
87
|
this.logger.debug(`Sent rejoin for ${this.identity.mateId}`);
|
|
73
88
|
}
|
|
74
89
|
});
|
|
75
|
-
this.ws.on("message", (data) => this.handleMessage(data.toString()));
|
|
76
|
-
this.ws.on("close", () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
this.ws.on("message", (data) => this.handleMessage(data.toString()));
|
|
91
|
+
this.ws.on("close", () => {
|
|
92
|
+
this.ws = null;
|
|
93
|
+
this.rejectPendingRequests("Focus websocket closed");
|
|
94
|
+
if (!this.stopped) {
|
|
95
|
+
this.reconnectTimeout = setTimeout(() => this.connect(), 2000);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
this.ws.on("error", () => {});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private handleMessage(data: string): void {
|
|
102
|
+
try {
|
|
103
|
+
const msg = JSON.parse(data);
|
|
104
|
+
this.tryResolvePendingRequest(msg);
|
|
83
105
|
if (msg.type === "join_ack" && msg.authKey && this.identity) {
|
|
84
106
|
this.identity.authKey = msg.authKey;
|
|
85
107
|
this.saveIdentity();
|
|
@@ -92,11 +114,88 @@ export class FocusForwarderService {
|
|
|
92
114
|
this.clearAuthKey();
|
|
93
115
|
} else if (msg.type === "leave_ack") {
|
|
94
116
|
this.logger.info("Left Focus world");
|
|
95
|
-
}
|
|
96
|
-
} catch (e) {
|
|
97
|
-
this.logger.warn(`Failed to parse message: ${e}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
this.logger.warn(`Failed to parse message: ${e}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private tryResolvePendingRequest(msg: { type?: unknown; requestId?: unknown }): void {
|
|
124
|
+
const requestId = typeof msg.requestId === "string" ? msg.requestId : "";
|
|
125
|
+
if (!requestId) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const pending = this.pendingRequests.get(requestId);
|
|
129
|
+
if (!pending) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (msg.type !== pending.expectedType) {
|
|
133
|
+
pending.reject(
|
|
134
|
+
new Error(
|
|
135
|
+
`Unexpected response type for request ${requestId}: ${String(msg.type)} (expected ${pending.expectedType})`,
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
clearTimeout(pending.timeout);
|
|
139
|
+
this.pendingRequests.delete(requestId);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
clearTimeout(pending.timeout);
|
|
143
|
+
this.pendingRequests.delete(requestId);
|
|
144
|
+
pending.resolve(msg);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private rejectPendingRequests(reason: string): void {
|
|
148
|
+
for (const [requestId, pending] of this.pendingRequests.entries()) {
|
|
149
|
+
clearTimeout(pending.timeout);
|
|
150
|
+
pending.reject(new Error(`${reason} (${requestId})`));
|
|
151
|
+
}
|
|
152
|
+
this.pendingRequests.clear();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private requireIdentity(): { mateId: string; authKey: string } | null {
|
|
156
|
+
if (!this.identity?.mateId || !this.identity?.authKey) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
mateId: this.identity.mateId,
|
|
161
|
+
authKey: this.identity.authKey,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private sendRequest<TResponse extends { type?: unknown; requestId?: unknown }>(
|
|
166
|
+
payload: { type: string; requestId?: string },
|
|
167
|
+
expectedType: string,
|
|
168
|
+
timeoutMs = 10000,
|
|
169
|
+
): Promise<TResponse> {
|
|
170
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
171
|
+
return Promise.reject(new Error("Focus websocket is not connected"));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const requestId = payload.requestId?.trim() || randomUUID();
|
|
175
|
+
const outboundPayload = { ...payload, requestId };
|
|
176
|
+
|
|
177
|
+
return new Promise<TResponse>((resolve, reject) => {
|
|
178
|
+
const timeout = setTimeout(() => {
|
|
179
|
+
this.pendingRequests.delete(requestId);
|
|
180
|
+
reject(new Error(`Timed out waiting for ${expectedType}`));
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
|
|
183
|
+
this.pendingRequests.set(requestId, {
|
|
184
|
+
expectedType,
|
|
185
|
+
timeout,
|
|
186
|
+
resolve: (value) => resolve(value as TResponse),
|
|
187
|
+
reject,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
this.ws?.send(JSON.stringify(outboundPayload));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
clearTimeout(timeout);
|
|
194
|
+
this.pendingRequests.delete(requestId);
|
|
195
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
100
199
|
|
|
101
200
|
private loadIdentity(): FocusIdentity | null {
|
|
102
201
|
try {
|
|
@@ -180,6 +279,49 @@ export class FocusForwarderService {
|
|
|
180
279
|
return true;
|
|
181
280
|
}
|
|
182
281
|
|
|
282
|
+
async queryNotesBoard(requestId?: string): Promise<QueryNotesBoardResultPayload> {
|
|
283
|
+
const identity = this.requireIdentity();
|
|
284
|
+
if (!identity) {
|
|
285
|
+
throw new Error("Missing Focus identity");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const payload: QueryNotesBoardPayload = {
|
|
289
|
+
type: "query_notes_board",
|
|
290
|
+
requestId: requestId?.trim() || randomUUID(),
|
|
291
|
+
mateId: identity.mateId,
|
|
292
|
+
authKey: identity.authKey,
|
|
293
|
+
};
|
|
294
|
+
return this.sendRequest<QueryNotesBoardResultPayload>(payload, "query_notes_board_result");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async createNotesBoardNote(
|
|
298
|
+
propId: string,
|
|
299
|
+
data: string,
|
|
300
|
+
requestId?: string,
|
|
301
|
+
): Promise<CreateNotesBoardNoteResultPayload> {
|
|
302
|
+
const identity = this.requireIdentity();
|
|
303
|
+
if (!identity) {
|
|
304
|
+
throw new Error("Missing Focus identity");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (data.trim().length > MAX_NOTEBOARD_TEXT_LENGTH) {
|
|
308
|
+
throw new Error(`Note content must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const payload: CreateNotesBoardNotePayload = {
|
|
312
|
+
type: "create_notes_board_note",
|
|
313
|
+
requestId: requestId?.trim() || randomUUID(),
|
|
314
|
+
mateId: identity.mateId,
|
|
315
|
+
authKey: identity.authKey,
|
|
316
|
+
propId,
|
|
317
|
+
data,
|
|
318
|
+
};
|
|
319
|
+
return this.sendRequest<CreateNotesBoardNoteResultPayload>(
|
|
320
|
+
payload,
|
|
321
|
+
"create_notes_board_note_result",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
183
325
|
isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
|
|
184
326
|
|
|
185
327
|
hasValidIdentity(): boolean { return !!this.identity?.mateId && !!this.identity?.authKey; }
|
package/src/types.ts
CHANGED
|
@@ -23,6 +23,16 @@ export type FocusIdentity = {
|
|
|
23
23
|
authKey?: string;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
export type FocusErrorResult = {
|
|
27
|
+
success: false;
|
|
28
|
+
errorCode?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
message?: string;
|
|
31
|
+
dailyLimit?: number;
|
|
32
|
+
remaining?: number;
|
|
33
|
+
resetAtUtc?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
26
36
|
export type JoinPayload = {
|
|
27
37
|
type: "join";
|
|
28
38
|
mateId: string;
|
|
@@ -101,3 +111,87 @@ export type ClockControlPayload = ClockPayloadBase & {
|
|
|
101
111
|
};
|
|
102
112
|
|
|
103
113
|
export type ClockPayload = ClockSetPayload | ClockControlPayload;
|
|
114
|
+
|
|
115
|
+
export type QueryNotesBoardNote = {
|
|
116
|
+
creatorName: string;
|
|
117
|
+
isCreatedByCurrentMate: boolean;
|
|
118
|
+
createTime: string;
|
|
119
|
+
updateTime: string;
|
|
120
|
+
data: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type QueryNotesBoard = {
|
|
124
|
+
propId: string;
|
|
125
|
+
noteCount: number;
|
|
126
|
+
latestActivityAt: string;
|
|
127
|
+
notes: QueryNotesBoardNote[];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type QueryNotesBoardPayload = {
|
|
131
|
+
type: "query_notes_board";
|
|
132
|
+
requestId: string;
|
|
133
|
+
mateId: string;
|
|
134
|
+
authKey: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type QueryNotesBoardSuccessPayload = {
|
|
138
|
+
type: "query_notes_board_result";
|
|
139
|
+
requestId: string;
|
|
140
|
+
success: true;
|
|
141
|
+
mateId: string;
|
|
142
|
+
spaceId: string;
|
|
143
|
+
dailyLimit: number;
|
|
144
|
+
remaining: number;
|
|
145
|
+
resetAtUtc: string;
|
|
146
|
+
boards: QueryNotesBoard[];
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type QueryNotesBoardFailurePayload = {
|
|
150
|
+
type: "query_notes_board_result";
|
|
151
|
+
requestId: string;
|
|
152
|
+
} & FocusErrorResult;
|
|
153
|
+
|
|
154
|
+
export type QueryNotesBoardResultPayload =
|
|
155
|
+
| QueryNotesBoardSuccessPayload
|
|
156
|
+
| QueryNotesBoardFailurePayload;
|
|
157
|
+
|
|
158
|
+
export type CreateNotesBoardNotePayload = {
|
|
159
|
+
type: "create_notes_board_note";
|
|
160
|
+
requestId: string;
|
|
161
|
+
mateId: string;
|
|
162
|
+
authKey: string;
|
|
163
|
+
propId: string;
|
|
164
|
+
data: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export type CreateNotesBoardNote = {
|
|
168
|
+
id: string;
|
|
169
|
+
ownerName: string;
|
|
170
|
+
createTime: string;
|
|
171
|
+
data: string;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
type NotesBoardMutationSuccessPayloadBase = {
|
|
175
|
+
requestId: string;
|
|
176
|
+
success: true;
|
|
177
|
+
mateId: string;
|
|
178
|
+
spaceId?: string;
|
|
179
|
+
propId: string;
|
|
180
|
+
dailyLimit?: number;
|
|
181
|
+
remaining?: number;
|
|
182
|
+
resetAtUtc?: string;
|
|
183
|
+
note: CreateNotesBoardNote;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export type CreateNotesBoardNoteSuccessPayload = NotesBoardMutationSuccessPayloadBase & {
|
|
187
|
+
type: "create_notes_board_note_result";
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type CreateNotesBoardNoteFailurePayload = {
|
|
191
|
+
type: "create_notes_board_note_result";
|
|
192
|
+
requestId: string;
|
|
193
|
+
} & FocusErrorResult;
|
|
194
|
+
|
|
195
|
+
export type CreateNotesBoardNoteResultPayload =
|
|
196
|
+
| CreateNotesBoardNoteSuccessPayload
|
|
197
|
+
| CreateNotesBoardNoteFailurePayload;
|