mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
Merge branch 'main' into feat/19809-slack-typing-reaction
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -17,15 +17,22 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to `agent:main`. Fixes #22188 and #26129. Thanks @paulomcg and @daht-mad.
|
||||
- Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to `agent:main`. (#34045) Thanks @paulomcg, @daht-mad and @vincentkoc.
|
||||
- Gateway/HTTP tools invoke media compatibility: preserve raw media payload access for direct `/tools/invoke` clients by allowing media `nodes` invoke commands only in HTTP tool context, while keeping agent-context media invoke blocking to prevent base64 prompt bloat. (#34365) Thanks @obviyus.
|
||||
- Agents/Nodes media outputs: add dedicated `photos_latest` action handling, block media-returning `nodes invoke` commands, keep metadata-only `camera.list` invoke allowed, and normalize empty `photos_latest` results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus.
|
||||
- TUI/session-key canonicalization: normalize `openclaw tui --session` values to lowercase so uppercase session names no longer drop real-time streaming updates due to gateway/TUI key mismatches. (#33866, #34013) thanks @lynnzc.
|
||||
- Outbound/send config threading: pass resolved SecretRef config through outbound adapters and helper send paths so send flows do not reload unresolved runtime config. (#33987) Thanks @joshavant.
|
||||
- Sessions/subagent attachments: remove `attachments[].content.maxLength` from `sessions_spawn` schema to avoid llama.cpp GBNF repetition overflow, and preflight UTF-8 byte size before buffer allocation while keeping runtime file-size enforcement unchanged. (#33648) Thanks @anisoptera.
|
||||
- Runtime/tool-state stability: recover from dangling Anthropic `tool_use` after compaction, serialize long-running Discord handler runs without blocking new inbound events, and prevent stale busy snapshots from suppressing stuck-channel recovery. (from #33630, #33583) Thanks @kevinWangSheng and @theotarr.
|
||||
- ACP/Discord startup hardening: clean up stuck ACP worker children on gateway restart, unbind stale ACP thread bindings during Discord startup reconciliation, and add per-thread listener watchdog timeouts so wedged turns cannot block later messages. (#33699) Thanks @dutifulbob.
|
||||
- Extensions/media local-root propagation: consistently forward `mediaLocalRoots` through extension `sendMedia` adapters (Google Chat, Slack, iMessage, Signal, WhatsApp), preserving non-local media behavior while restoring local attachment resolution from configured roots. Synthesis of #33581, #33545, #33540, #33536, #33528. Thanks @bmendonca3.
|
||||
- Gateway/security default response headers: add `Permissions-Policy: camera=(), microphone=(), geolocation=()` to baseline gateway HTTP security headers for all responses. (#30186) thanks @habakan.
|
||||
- Plugins/startup loading: lazily initialize plugin runtime, split startup-critical plugin SDK imports into `openclaw/plugin-sdk/core` and `openclaw/plugin-sdk/telegram`, and preserve `api.runtime` reflection semantics for plugin compatibility. (#28620) thanks @hmemcpy.
|
||||
- Plugins/startup performance: reduce bursty plugin discovery/manifest overhead with short in-process caches, skip importing bundled memory plugins that are disabled by slot selection, and speed legacy root `openclaw/plugin-sdk` compatibility via runtime root-alias routing while preserving backward compatibility. Thanks @gumadeiras.
|
||||
- Build/lazy runtime boundaries: replace ineffective dynamic import sites with dedicated lazy runtime boundaries across Slack slash handling, Telegram audit, CLI send deps, memory fallback, and outbound delivery paths while preserving behavior. (#33690) thanks @gumadeiras.
|
||||
- Config/heartbeat legacy-path handling: auto-migrate top-level `heartbeat` into `agents.defaults.heartbeat` (with merge semantics that preserve explicit defaults), and keep startup failures on non-migratable legacy entries in the detailed invalid-config path instead of generic migration-failed errors. (#32706) thanks @xiwan.
|
||||
- Plugins/SDK subpath parity: add channel-specific plugin SDK subpaths for Discord, Slack, Signal, iMessage, WhatsApp, and LINE; migrate bundled plugin entrypoints to scoped subpaths/core with CI guardrails; and keep `openclaw/plugin-sdk` root import compatibility for existing external plugins. (#33737) thanks @gumadeiras.
|
||||
- Plugins/SDK subpath parity: expand plugin SDK subpaths across bundled channels/extensions (Discord, Slack, Signal, iMessage, WhatsApp, LINE, and bundled companion plugins), with build/export/type/runtime wiring so scoped imports resolve consistently in source and dist while preserving compatibility. (#33737) thanks @gumadeiras.
|
||||
- Plugins/bundled scoped-import migration: migrate bundled plugins from monolithic `openclaw/plugin-sdk` imports to scoped subpaths (or `openclaw/plugin-sdk/core`) across registration and startup-sensitive runtime files, add CI/release guardrails to prevent regressions, and keep root `openclaw/plugin-sdk` support for external/community plugins. Thanks @gumadeiras.
|
||||
- Routing/session duplicate suppression synthesis: align shared session delivery-context inheritance, channel-paired route-field merges, and reply-surface target matching so dmScope=main turns avoid cross-surface duplicate replies while thread-aware forwarding keeps intended routing semantics. (from #33629, #26889, #17337, #33250) Thanks @Yuandiaodiaodiao, @kevinwildenradt, @Glucksberg, and @bmendonca3.
|
||||
- Routing/legacy session route inheritance: preserve external route metadata inheritance for legacy channel session keys (`agent:<agent>:<channel>:<peer>` and `...:thread:<id>`) so `chat.send` does not incorrectly fall back to webchat when valid delivery context exists. Follow-up to #33786.
|
||||
- Routing/legacy route guard tightening: require legacy session-key channel hints to match the saved delivery channel before inheriting external routing metadata, preventing custom namespaced keys like `agent:<agent>:work:<ticket>` from inheriting stale non-webchat routes.
|
||||
@@ -51,6 +58,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord/auto presence health signal: add runtime availability-driven presence updates plus connected-state reporting to improve health monitoring and operator visibility. (#33277) Thanks @thewilloftheshadow.
|
||||
- Telegram/draft-stream boundary stability: materialize DM draft previews at assistant-message/tool boundaries, serialize lane-boundary callbacks before final delivery, and scope preview cleanup to the active preview so multi-step Telegram streams no longer lose, overwrite, or leave stale preview bubbles. (#33842) Thanks @ngutman.
|
||||
- Telegram/DM draft finalization reliability: require verified final-text draft emission before treating preview finalization as delivered, and fall back to normal payload send when final draft delivery is not confirmed (preventing missing final responses and preserving media/button delivery). (#32118) Thanks @OpenCils.
|
||||
- Telegram/DM draft final delivery: materialize text-only `sendMessageDraft` previews into one permanent final message and skip duplicate final payload sends, while preserving fallback behavior when materialization fails. (#34318) Thanks @Brotherinlaw-13.
|
||||
- Telegram/draft preview boundary + silent-token reliability: stabilize answer-lane message boundaries across late-partial/message-start races, preserve/reset finalized preview state at the correct boundaries, and suppress `NO_REPLY` lead-fragment leaks without broad heartbeat-prefix false positives. (#33169) Thanks @obviyus.
|
||||
- Discord/audit wildcard warnings: ignore "\*" wildcard keys when counting unresolved guild channels so doctor/status no longer warns on allow-all configs. (#33125) Thanks @thewilloftheshadow.
|
||||
- Discord/channel resolution: default bare numeric recipients to channels, harden allowlist numeric ID handling with safe fallbacks, and avoid inbound WS heartbeat stalls. (#33142) Thanks @thewilloftheshadow.
|
||||
@@ -66,12 +74,17 @@ Docs: https://docs.openclaw.ai
|
||||
- iOS/TTS playback fallback: keep voice playback resilient by switching from PCM to MP3 when provider format support is unavailable, while avoiding sticky fallback on generic local playback errors. (#33032) thanks @mbelinky.
|
||||
- Telegram/multi-account default routing clarity: warn only for ambiguous (2+) account setups without an explicit default, add `openclaw doctor` warnings for missing/invalid multi-account defaults across channels, and document explicit-default guidance for channel routing and Telegram config. (#32544) thanks @Sid-Qin.
|
||||
- Telegram/plugin outbound hook parity: run `message_sending` + `message_sent` in Telegram reply delivery, include reply-path hook metadata (`mediaUrls`, `threadId`), and report `message_sent.success=false` when hooks blank text and no outbound message is delivered. (#32649) Thanks @KimGLee.
|
||||
- CLI/Coding-agent reliability: switch default `claude-cli` non-interactive args to `--permission-mode bypassPermissions`, auto-normalize legacy `--dangerously-skip-permissions` backend overrides to the modern permission-mode form, align coding-agent + live-test docs with the non-PTY Claude path, and emit session system-event heartbeat notices when CLI watchdog no-output timeouts terminate runs. (#28610, #31149, #34055). Thanks @niceysam, @cryptomaltese and @vincentkoc.
|
||||
- ACP/ACPX session bootstrap: retry with `sessions new` when `sessions ensure` returns no session identifiers so ACP spawns avoid `NO_SESSION`/`ACP_TURN_FAILED` failures on affected agents. (#28786, #31338, #34055). Thanks @Sid-Qin and @vincentkoc.
|
||||
- ACP/sessions_spawn parent stream visibility: add `streamTo: "parent"` for `runtime: "acp"` to forward initial child-run progress/no-output/completion updates back into the requester session as system events (instead of direct child delivery), and emit a tail-able session-scoped relay log (`<sessionId>.acp-stream.jsonl`, returned as `streamLogPath` when available), improving orchestrator visibility for blocked or long-running harness turns. (#34310, #29909; reopened from #34055). Thanks @vincentkoc.
|
||||
- Agents/bootstrap truncation warning handling: unify bootstrap budget/truncation analysis across embedded + CLI runtime, `/context`, and `openclaw doctor`; add `agents.defaults.bootstrapPromptTruncationWarning` (`off|once|always`, default `once`) and persist warning-signature metadata so truncation warnings are consistent and deduped across turns. (#32769) Thanks @gumadeiras.
|
||||
- Agents/Skills runtime loading: propagate run config into embedded attempt and compaction skill-entry loading so explicitly enabled bundled companion skills are discovered consistently when skill snapshots do not already provide resolved entries. Thanks @gumadeiras.
|
||||
- Agents/Session startup date grounding: substitute `YYYY-MM-DD` placeholders in startup/post-compaction AGENTS context and append runtime current-time lines for `/new` and `/reset` prompts so daily-memory references resolve correctly. (#32381) Thanks @chengzhichao-xydt.
|
||||
- Agents/Compaction continuity: expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context. (#8903) thanks @joetomasone.
|
||||
- Gateway/status self version reporting: make Gateway self version in `openclaw status` prefer runtime `VERSION` (while preserving explicit `OPENCLAW_VERSION` override), preventing stale post-upgrade app version output. (#32655) thanks @liuxiaopai-ai.
|
||||
- Memory/QMD index isolation: set `QMD_CONFIG_DIR` alongside `XDG_CONFIG_HOME` so QMD config state stays per-agent despite upstream XDG handling bugs, preventing cross-agent collection indexing and excess disk/CPU usage. (#27028) thanks @HenryLoenwind.
|
||||
- CLI/Coding-agent reliability: switch default `claude-cli` non-interactive args to `--permission-mode bypassPermissions`, auto-normalize legacy `--dangerously-skip-permissions` backend overrides to the modern permission-mode form, align coding-agent + live-test docs with the non-PTY Claude path, and emit session system-event heartbeat notices when CLI watchdog no-output timeouts terminate runs. Related to #28261. Landed from contributor PRs #28610 and #31149. Thanks @niceysam, @cryptomaltese and @vincentkoc.
|
||||
- ACP/ACPX session bootstrap: retry with `sessions new` when `sessions ensure` returns no session identifiers so ACP spawns avoid `NO_SESSION`/`ACP_TURN_FAILED` failures on affected agents. Related to #28786. Landed from contributor PR #31338. Thanks @Sid-Qin and @vincentkoc.
|
||||
- LINE/auth boundary hardening synthesis: enforce strict LINE webhook authn/z boundary semantics across pairing-store account scoping, DM/group allowlist separation, fail-closed webhook auth/runtime behavior, and replay/duplication controls (including in-flight replay reservation and post-success dedupe marking). (from #26701, #26683, #25978, #17593, #16619, #31990, #26047, #30584, #18777) Thanks @bmendonca3, @davidahmann, @harshang03, @haosenwang1018, @liuxiaopai-ai, @coygeek, and @Takhoffman.
|
||||
- LINE/media download synthesis: fix file-media download handling and M4A audio classification across overlapping LINE regressions. (from #26386, #27761, #27787, #29509, #29755, #29776, #29785, #32240) Thanks @kevinWangSheng, @loiie45e, @carrotRakko, @Sid-Qin, @codeafridi, and @bmendonca3.
|
||||
- LINE/context and routing synthesis: fix group/room peer routing and command-authorization context propagation, and keep processing later events in mixed-success webhook batches. (from #21955, #24475, #27035, #28286) Thanks @lailoo, @mcaxtr, @jervyclaw, @Glucksberg, and @Takhoffman.
|
||||
|
||||
6
apps/ios/ActivityWidget/Assets.xcassets/Contents.json
Normal file
6
apps/ios/ActivityWidget/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
31
apps/ios/ActivityWidget/Info.plist
Normal file
31
apps/ios/ActivityWidget/Info.plist
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>OpenClaw Activity</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.3.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>20260301</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@main
|
||||
struct OpenClawActivityWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
OpenClawLiveActivity()
|
||||
}
|
||||
}
|
||||
84
apps/ios/ActivityWidget/OpenClawLiveActivity.swift
Normal file
84
apps/ios/ActivityWidget/OpenClawLiveActivity.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
import ActivityKit
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct OpenClawLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: OpenClawActivityAttributes.self) { context in
|
||||
lockScreenView(context: context)
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
statusDot(state: context.state)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.center) {
|
||||
Text(context.state.statusText)
|
||||
.font(.subheadline)
|
||||
.lineLimit(1)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
trailingView(state: context.state)
|
||||
}
|
||||
} compactLeading: {
|
||||
statusDot(state: context.state)
|
||||
} compactTrailing: {
|
||||
Text(context.state.statusText)
|
||||
.font(.caption2)
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: 64)
|
||||
} minimal: {
|
||||
statusDot(state: context.state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func lockScreenView(context: ActivityViewContext<OpenClawActivityAttributes>) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
statusDot(state: context.state)
|
||||
.frame(width: 10, height: 10)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("OpenClaw")
|
||||
.font(.subheadline.bold())
|
||||
Text(context.state.statusText)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
trailingView(state: context.state)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func trailingView(state: OpenClawActivityAttributes.ContentState) -> some View {
|
||||
if state.isConnecting {
|
||||
ProgressView().controlSize(.small)
|
||||
} else if state.isDisconnected {
|
||||
Image(systemName: "wifi.slash")
|
||||
.foregroundStyle(.red)
|
||||
} else if state.isIdle {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.foregroundStyle(.green)
|
||||
} else {
|
||||
Text(state.startedAt, style: .timer)
|
||||
.font(.caption)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func statusDot(state: OpenClawActivityAttributes.ContentState) -> some View {
|
||||
Circle()
|
||||
.fill(dotColor(state: state))
|
||||
.frame(width: 6, height: 6)
|
||||
}
|
||||
|
||||
private func dotColor(state: OpenClawActivityAttributes.ContentState) -> Color {
|
||||
if state.isDisconnected { return .red }
|
||||
if state.isConnecting { return .gray }
|
||||
if state.isIdle { return .green }
|
||||
return .blue
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM)
|
||||
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios
|
||||
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.watchkitapp
|
||||
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.watchkitapp.extension
|
||||
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.ios.activitywidget
|
||||
|
||||
// Local contributors can override this by running scripts/ios-configure-signing.sh.
|
||||
// Keep include after defaults: xcconfig is evaluated top-to-bottom.
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
<string>OpenClaw needs microphone access for voice wake.</string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
<string>OpenClaw uses on-device speech recognition for voice wake.</string>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
||||
125
apps/ios/Sources/LiveActivity/LiveActivityManager.swift
Normal file
125
apps/ios/Sources/LiveActivity/LiveActivityManager.swift
Normal file
@@ -0,0 +1,125 @@
|
||||
import ActivityKit
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
/// Minimal Live Activity lifecycle focused on connection health + stale cleanup.
|
||||
@MainActor
|
||||
final class LiveActivityManager {
|
||||
static let shared = LiveActivityManager()
|
||||
|
||||
private let logger = Logger(subsystem: "ai.openclaw.ios", category: "LiveActivity")
|
||||
private var currentActivity: Activity<OpenClawActivityAttributes>?
|
||||
private var activityStartDate: Date = .now
|
||||
|
||||
private init() {
|
||||
self.hydrateCurrentAndPruneDuplicates()
|
||||
}
|
||||
|
||||
var isActive: Bool {
|
||||
guard let activity = self.currentActivity else { return false }
|
||||
guard activity.activityState == .active else {
|
||||
self.currentActivity = nil
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func startActivity(agentName: String, sessionKey: String) {
|
||||
self.hydrateCurrentAndPruneDuplicates()
|
||||
|
||||
if self.currentActivity != nil {
|
||||
self.handleConnecting()
|
||||
return
|
||||
}
|
||||
|
||||
let authInfo = ActivityAuthorizationInfo()
|
||||
guard authInfo.areActivitiesEnabled else {
|
||||
self.logger.info("Live Activities disabled; skipping start")
|
||||
return
|
||||
}
|
||||
|
||||
self.activityStartDate = .now
|
||||
let attributes = OpenClawActivityAttributes(agentName: agentName, sessionKey: sessionKey)
|
||||
|
||||
do {
|
||||
let activity = try Activity.request(
|
||||
attributes: attributes,
|
||||
content: ActivityContent(state: self.connectingState(), staleDate: nil),
|
||||
pushType: nil)
|
||||
self.currentActivity = activity
|
||||
self.logger.info("started live activity id=\(activity.id, privacy: .public)")
|
||||
} catch {
|
||||
self.logger.error("failed to start live activity: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnecting() {
|
||||
self.updateCurrent(state: self.connectingState())
|
||||
}
|
||||
|
||||
func handleReconnect() {
|
||||
self.updateCurrent(state: self.idleState())
|
||||
}
|
||||
|
||||
func handleDisconnect() {
|
||||
self.updateCurrent(state: self.disconnectedState())
|
||||
}
|
||||
|
||||
private func hydrateCurrentAndPruneDuplicates() {
|
||||
let active = Activity<OpenClawActivityAttributes>.activities
|
||||
guard !active.isEmpty else {
|
||||
self.currentActivity = nil
|
||||
return
|
||||
}
|
||||
|
||||
let keeper = active.max { lhs, rhs in
|
||||
lhs.content.state.startedAt < rhs.content.state.startedAt
|
||||
} ?? active[0]
|
||||
|
||||
self.currentActivity = keeper
|
||||
self.activityStartDate = keeper.content.state.startedAt
|
||||
|
||||
let stale = active.filter { $0.id != keeper.id }
|
||||
for activity in stale {
|
||||
Task {
|
||||
await activity.end(
|
||||
ActivityContent(state: self.disconnectedState(), staleDate: nil),
|
||||
dismissalPolicy: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCurrent(state: OpenClawActivityAttributes.ContentState) {
|
||||
guard let activity = self.currentActivity else { return }
|
||||
Task {
|
||||
await activity.update(ActivityContent(state: state, staleDate: nil))
|
||||
}
|
||||
}
|
||||
|
||||
private func connectingState() -> OpenClawActivityAttributes.ContentState {
|
||||
OpenClawActivityAttributes.ContentState(
|
||||
statusText: "Connecting...",
|
||||
isIdle: false,
|
||||
isDisconnected: false,
|
||||
isConnecting: true,
|
||||
startedAt: self.activityStartDate)
|
||||
}
|
||||
|
||||
private func idleState() -> OpenClawActivityAttributes.ContentState {
|
||||
OpenClawActivityAttributes.ContentState(
|
||||
statusText: "Idle",
|
||||
isIdle: true,
|
||||
isDisconnected: false,
|
||||
isConnecting: false,
|
||||
startedAt: self.activityStartDate)
|
||||
}
|
||||
|
||||
private func disconnectedState() -> OpenClawActivityAttributes.ContentState {
|
||||
OpenClawActivityAttributes.ContentState(
|
||||
statusText: "Disconnected",
|
||||
isIdle: false,
|
||||
isDisconnected: true,
|
||||
isConnecting: false,
|
||||
startedAt: self.activityStartDate)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import ActivityKit
|
||||
import Foundation
|
||||
|
||||
/// Shared schema used by iOS app + Live Activity widget extension.
|
||||
struct OpenClawActivityAttributes: ActivityAttributes {
|
||||
var agentName: String
|
||||
var sessionKey: String
|
||||
|
||||
struct ContentState: Codable, Hashable {
|
||||
var statusText: String
|
||||
var isIdle: Bool
|
||||
var isDisconnected: Bool
|
||||
var isConnecting: Bool
|
||||
var startedAt: Date
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension OpenClawActivityAttributes {
|
||||
static let preview = OpenClawActivityAttributes(agentName: "main", sessionKey: "main")
|
||||
}
|
||||
|
||||
extension OpenClawActivityAttributes.ContentState {
|
||||
static let connecting = OpenClawActivityAttributes.ContentState(
|
||||
statusText: "Connecting...",
|
||||
isIdle: false,
|
||||
isDisconnected: false,
|
||||
isConnecting: true,
|
||||
startedAt: .now)
|
||||
|
||||
static let idle = OpenClawActivityAttributes.ContentState(
|
||||
statusText: "Idle",
|
||||
isIdle: true,
|
||||
isDisconnected: false,
|
||||
isConnecting: false,
|
||||
startedAt: .now)
|
||||
|
||||
static let disconnected = OpenClawActivityAttributes.ContentState(
|
||||
statusText: "Disconnected",
|
||||
isIdle: false,
|
||||
isDisconnected: true,
|
||||
isConnecting: false,
|
||||
startedAt: .now)
|
||||
}
|
||||
#endif
|
||||
@@ -1695,6 +1695,7 @@ extension NodeAppModel {
|
||||
self.operatorGatewayTask = nil
|
||||
self.voiceWakeSyncTask?.cancel()
|
||||
self.voiceWakeSyncTask = nil
|
||||
LiveActivityManager.shared.handleDisconnect()
|
||||
self.gatewayHealthMonitor.stop()
|
||||
Task {
|
||||
await self.operatorGateway.disconnect()
|
||||
@@ -1731,6 +1732,7 @@ private extension NodeAppModel {
|
||||
self.operatorConnected = false
|
||||
self.voiceWakeSyncTask?.cancel()
|
||||
self.voiceWakeSyncTask = nil
|
||||
LiveActivityManager.shared.handleDisconnect()
|
||||
self.gatewayDefaultAgentId = nil
|
||||
self.gatewayAgents = []
|
||||
self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID)
|
||||
@@ -1811,6 +1813,7 @@ private extension NodeAppModel {
|
||||
await self.refreshAgentsFromGateway()
|
||||
await self.refreshShareRouteFromGateway()
|
||||
await self.startVoiceWakeSync()
|
||||
await MainActor.run { LiveActivityManager.shared.handleReconnect() }
|
||||
await MainActor.run { self.startGatewayHealthMonitor() }
|
||||
},
|
||||
onDisconnected: { [weak self] reason in
|
||||
@@ -1818,6 +1821,7 @@ private extension NodeAppModel {
|
||||
await MainActor.run {
|
||||
self.operatorConnected = false
|
||||
self.talkMode.updateGatewayConnected(false)
|
||||
LiveActivityManager.shared.handleDisconnect()
|
||||
}
|
||||
GatewayDiagnostics.log("operator gateway disconnected reason=\(reason)")
|
||||
await MainActor.run { self.stopGatewayHealthMonitor() }
|
||||
@@ -1882,6 +1886,14 @@ private extension NodeAppModel {
|
||||
self.gatewayStatusText = (attempt == 0) ? "Connecting…" : "Reconnecting…"
|
||||
self.gatewayServerName = nil
|
||||
self.gatewayRemoteAddress = nil
|
||||
let liveActivity = LiveActivityManager.shared
|
||||
if liveActivity.isActive {
|
||||
liveActivity.handleConnecting()
|
||||
} else {
|
||||
liveActivity.startActivity(
|
||||
agentName: self.selectedAgentId ?? "main",
|
||||
sessionKey: self.mainSessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
@@ -62,3 +62,7 @@ Sources/Voice/VoiceWakePreferences.swift
|
||||
../../Swabble/Sources/SwabbleKit/WakeWordGate.swift
|
||||
Sources/Voice/TalkModeManager.swift
|
||||
Sources/Voice/TalkOrbOverlay.swift
|
||||
Sources/LiveActivity/OpenClawActivityAttributes.swift
|
||||
Sources/LiveActivity/LiveActivityManager.swift
|
||||
ActivityWidget/OpenClawActivityWidgetBundle.swift
|
||||
ActivityWidget/OpenClawLiveActivity.swift
|
||||
|
||||
@@ -38,6 +38,8 @@ targets:
|
||||
dependencies:
|
||||
- target: OpenClawShareExtension
|
||||
embed: true
|
||||
- target: OpenClawActivityWidget
|
||||
embed: true
|
||||
- target: OpenClawWatchApp
|
||||
- package: OpenClawKit
|
||||
- package: OpenClawKit
|
||||
@@ -84,6 +86,7 @@ targets:
|
||||
TARGETED_DEVICE_FAMILY: "1"
|
||||
SWIFT_VERSION: "6.0"
|
||||
SWIFT_STRICT_CONCURRENCY: complete
|
||||
SUPPORTS_LIVE_ACTIVITIES: YES
|
||||
ENABLE_APPINTENTS_METADATA: NO
|
||||
ENABLE_APP_INTENTS_METADATA_GENERATION: NO
|
||||
info:
|
||||
@@ -115,6 +118,7 @@ targets:
|
||||
NSLocationAlwaysAndWhenInUseUsageDescription: OpenClaw can share your location in the background when you enable Always.
|
||||
NSMicrophoneUsageDescription: OpenClaw needs microphone access for voice wake.
|
||||
NSSpeechRecognitionUsageDescription: OpenClaw uses on-device speech recognition for voice wake.
|
||||
NSSupportsLiveActivities: true
|
||||
UISupportedInterfaceOrientations:
|
||||
- UIInterfaceOrientationPortrait
|
||||
- UIInterfaceOrientationPortraitUpsideDown
|
||||
@@ -164,6 +168,37 @@ targets:
|
||||
NSExtensionActivationSupportsImageWithMaxCount: 10
|
||||
NSExtensionActivationSupportsMovieWithMaxCount: 1
|
||||
|
||||
OpenClawActivityWidget:
|
||||
type: app-extension
|
||||
platform: iOS
|
||||
configFiles:
|
||||
Debug: Signing.xcconfig
|
||||
Release: Signing.xcconfig
|
||||
sources:
|
||||
- path: ActivityWidget
|
||||
- path: Sources/LiveActivity/OpenClawActivityAttributes.swift
|
||||
dependencies:
|
||||
- sdk: WidgetKit.framework
|
||||
- sdk: ActivityKit.framework
|
||||
settings:
|
||||
base:
|
||||
CODE_SIGN_IDENTITY: "Apple Development"
|
||||
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
|
||||
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
|
||||
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID)"
|
||||
SWIFT_VERSION: "6.0"
|
||||
SWIFT_STRICT_CONCURRENCY: complete
|
||||
SUPPORTS_LIVE_ACTIVITIES: YES
|
||||
info:
|
||||
path: ActivityWidget/Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: OpenClaw Activity
|
||||
CFBundleShortVersionString: "2026.3.2"
|
||||
CFBundleVersion: "20260301"
|
||||
NSSupportsLiveActivities: true
|
||||
NSExtension:
|
||||
NSExtensionPointIdentifier: com.apple.widgetkit-extension
|
||||
|
||||
OpenClawWatchApp:
|
||||
type: application.watchapp2
|
||||
platform: watchOS
|
||||
|
||||
1
changelog/fragments/ios-live-activity-status-cleanup.md
Normal file
1
changelog/fragments/ios-live-activity-status-cleanup.md
Normal file
@@ -0,0 +1 @@
|
||||
- iOS: add Live Activity connection status (connecting/idle/disconnected) on Lock Screen and Dynamic Island, and clean up duplicate/stale activities before starting a new one (#33591) (thanks @mbelinky, @leepokai)
|
||||
@@ -185,8 +185,8 @@ Input modes:
|
||||
OpenClaw ships a default for `claude-cli`:
|
||||
|
||||
- `command: "claude"`
|
||||
- `args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"]`
|
||||
- `resumeArgs: ["-p", "--output-format", "json", "--dangerously-skip-permissions", "--resume", "{sessionId}"]`
|
||||
- `args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"]`
|
||||
- `resumeArgs: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions", "--resume", "{sessionId}"]`
|
||||
- `modelArg: "--model"`
|
||||
- `systemPromptArg: "--append-system-prompt"`
|
||||
- `sessionArg: "--session-id"`
|
||||
|
||||
@@ -219,7 +219,7 @@ OPENCLAW_LIVE_SETUP_TOKEN=1 OPENCLAW_LIVE_SETUP_TOKEN_PROFILE=anthropic:setup-to
|
||||
- Defaults:
|
||||
- Model: `claude-cli/claude-sonnet-4-6`
|
||||
- Command: `claude`
|
||||
- Args: `["-p","--output-format","json","--dangerously-skip-permissions"]`
|
||||
- Args: `["-p","--output-format","json","--permission-mode","bypassPermissions"]`
|
||||
- Overrides (optional):
|
||||
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-opus-4-6"`
|
||||
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.3-codex"`
|
||||
|
||||
@@ -119,6 +119,8 @@ Interface details:
|
||||
- `mode: "session"` requires `thread: true`
|
||||
- `cwd` (optional): requested runtime working directory (validated by backend/runtime policy).
|
||||
- `label` (optional): operator-facing label used in session/banner text.
|
||||
- `streamTo` (optional): `"parent"` streams initial ACP run progress summaries back to the requester session as system events.
|
||||
- When available, accepted responses include `streamLogPath` pointing to a session-scoped JSONL log (`<sessionId>.acp-stream.jsonl`) you can tail for full relay history.
|
||||
|
||||
## Sandbox compatibility
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@ Core parameters:
|
||||
- `sessions_list`: `kinds?`, `limit?`, `activeMinutes?`, `messageLimit?` (0 = none)
|
||||
- `sessions_history`: `sessionKey` (or `sessionId`), `limit?`, `includeTools?`
|
||||
- `sessions_send`: `sessionKey` (or `sessionId`), `message`, `timeoutSeconds?` (0 = fire-and-forget)
|
||||
- `sessions_spawn`: `task`, `label?`, `runtime?`, `agentId?`, `model?`, `thinking?`, `cwd?`, `runTimeoutSeconds?`, `thread?`, `mode?`, `cleanup?`, `sandbox?`, `attachments?`, `attachAs?`
|
||||
- `sessions_spawn`: `task`, `label?`, `runtime?`, `agentId?`, `model?`, `thinking?`, `cwd?`, `runTimeoutSeconds?`, `thread?`, `mode?`, `cleanup?`, `sandbox?`, `streamTo?`, `attachments?`, `attachAs?`
|
||||
- `session_status`: `sessionKey?` (default current; accepts `sessionId`), `model?` (`default` clears override)
|
||||
|
||||
Notes:
|
||||
@@ -483,6 +483,7 @@ Notes:
|
||||
- `sessions_send` waits for final completion when `timeoutSeconds > 0`.
|
||||
- Delivery/announce happens after completion and is best-effort; `status: "ok"` confirms the agent run finished, not that the announce was delivered.
|
||||
- `sessions_spawn` supports `runtime: "subagent" | "acp"` (`subagent` default). For ACP runtime behavior, see [ACP Agents](/tools/acp-agents).
|
||||
- For ACP runtime, `streamTo: "parent"` routes initial-run progress summaries back to the requester session as system events instead of direct child delivery.
|
||||
- `sessions_spawn` starts a sub-agent run and posts an announce reply back to the requester chat.
|
||||
- Supports one-shot mode (`mode: "run"`) and persistent thread-bound mode (`mode: "session"` with `thread: true`).
|
||||
- If `thread: true` and `mode` is omitted, mode defaults to `session`.
|
||||
@@ -496,6 +497,7 @@ Notes:
|
||||
- Configure limits via `tools.sessions_spawn.attachments` (`enabled`, `maxTotalBytes`, `maxFiles`, `maxFileBytes`, `retainOnSessionKeep`).
|
||||
- `attachAs.mountPath` is a reserved hint for future mount implementations.
|
||||
- `sessions_spawn` is non-blocking and returns `status: "accepted"` immediately.
|
||||
- ACP `streamTo: "parent"` responses may include `streamLogPath` (session-scoped `*.acp-stream.jsonl`) for tailing progress history.
|
||||
- `sessions_send` runs a reply‑back ping‑pong (reply `REPLY_SKIP` to stop; max turns via `session.agentToAgent.maxPingPongTurns`, 0–5).
|
||||
- After the ping‑pong, the target agent runs an **announce step**; reply `ANNOUNCE_SKIP` to suppress the announcement.
|
||||
- Sandbox clamp: when the current session is sandboxed and `agents.defaults.sandbox.sessionToolsVisibility: "spawned"`, OpenClaw clamps `tools.sessions.visibility` to `tree`.
|
||||
|
||||
@@ -112,6 +112,7 @@ Use SDK subpaths instead of the monolithic `openclaw/plugin-sdk` import when
|
||||
authoring plugins:
|
||||
|
||||
- `openclaw/plugin-sdk/core` for generic plugin APIs, provider auth types, and shared helpers.
|
||||
- `openclaw/plugin-sdk/compat` for bundled/internal plugin code that needs broader shared runtime helpers than `core`.
|
||||
- `openclaw/plugin-sdk/telegram` for Telegram channel plugins.
|
||||
- `openclaw/plugin-sdk/discord` for Discord channel plugins.
|
||||
- `openclaw/plugin-sdk/slack` for Slack channel plugins.
|
||||
@@ -119,12 +120,41 @@ authoring plugins:
|
||||
- `openclaw/plugin-sdk/imessage` for iMessage channel plugins.
|
||||
- `openclaw/plugin-sdk/whatsapp` for WhatsApp channel plugins.
|
||||
- `openclaw/plugin-sdk/line` for LINE channel plugins.
|
||||
- `openclaw/plugin-sdk/msteams` for the bundled Microsoft Teams plugin surface.
|
||||
- Bundled extension-specific subpaths are also available:
|
||||
`openclaw/plugin-sdk/acpx`, `openclaw/plugin-sdk/bluebubbles`,
|
||||
`openclaw/plugin-sdk/copilot-proxy`, `openclaw/plugin-sdk/device-pair`,
|
||||
`openclaw/plugin-sdk/diagnostics-otel`, `openclaw/plugin-sdk/diffs`,
|
||||
`openclaw/plugin-sdk/feishu`,
|
||||
`openclaw/plugin-sdk/google-gemini-cli-auth`, `openclaw/plugin-sdk/googlechat`,
|
||||
`openclaw/plugin-sdk/irc`, `openclaw/plugin-sdk/llm-task`,
|
||||
`openclaw/plugin-sdk/lobster`, `openclaw/plugin-sdk/matrix`,
|
||||
`openclaw/plugin-sdk/mattermost`, `openclaw/plugin-sdk/memory-core`,
|
||||
`openclaw/plugin-sdk/memory-lancedb`,
|
||||
`openclaw/plugin-sdk/minimax-portal-auth`,
|
||||
`openclaw/plugin-sdk/nextcloud-talk`, `openclaw/plugin-sdk/nostr`,
|
||||
`openclaw/plugin-sdk/open-prose`, `openclaw/plugin-sdk/phone-control`,
|
||||
`openclaw/plugin-sdk/qwen-portal-auth`, `openclaw/plugin-sdk/synology-chat`,
|
||||
`openclaw/plugin-sdk/talk-voice`, `openclaw/plugin-sdk/test-utils`,
|
||||
`openclaw/plugin-sdk/thread-ownership`, `openclaw/plugin-sdk/tlon`,
|
||||
`openclaw/plugin-sdk/twitch`, `openclaw/plugin-sdk/voice-call`,
|
||||
`openclaw/plugin-sdk/zalo`, and `openclaw/plugin-sdk/zalouser`.
|
||||
|
||||
Compatibility note:
|
||||
|
||||
- `openclaw/plugin-sdk` remains supported for existing external plugins.
|
||||
- New and migrated bundled plugins should use channel subpaths (or `core`) to
|
||||
keep startup imports scoped.
|
||||
- New and migrated bundled plugins should use channel or extension-specific
|
||||
subpaths; use `core` for generic surfaces and `compat` only when broader
|
||||
shared helpers are required.
|
||||
|
||||
Performance note:
|
||||
|
||||
- Plugin discovery and manifest metadata use short in-process caches to reduce
|
||||
bursty startup/reload work.
|
||||
- Set `OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE=1` or
|
||||
`OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE=1` to disable these caches.
|
||||
- Tune cache windows with `OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS` and
|
||||
`OPENCLAW_PLUGIN_MANIFEST_CACHE_MS`.
|
||||
|
||||
## Discovery & precedence
|
||||
|
||||
@@ -144,13 +174,21 @@ OpenClaw scans, in order:
|
||||
- `~/.openclaw/extensions/*.ts`
|
||||
- `~/.openclaw/extensions/*/index.ts`
|
||||
|
||||
4. Bundled extensions (shipped with OpenClaw, **disabled by default**)
|
||||
4. Bundled extensions (shipped with OpenClaw, mostly disabled by default)
|
||||
|
||||
- `<openclaw>/extensions/*`
|
||||
|
||||
Bundled plugins must be enabled explicitly via `plugins.entries.<id>.enabled`
|
||||
or `openclaw plugins enable <id>`. Installed plugins are enabled by default,
|
||||
but can be disabled the same way.
|
||||
Most bundled plugins must be enabled explicitly via
|
||||
`plugins.entries.<id>.enabled` or `openclaw plugins enable <id>`.
|
||||
|
||||
Default-on bundled plugin exceptions:
|
||||
|
||||
- `device-pair`
|
||||
- `phone-control`
|
||||
- `talk-voice`
|
||||
- active memory slot plugin (default slot: `memory-core`)
|
||||
|
||||
Installed plugins are enabled by default, but can be disabled the same way.
|
||||
|
||||
Hardening notes:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/acpx";
|
||||
import { createAcpxPluginConfigSchema } from "./src/config.js";
|
||||
import { createAcpxRuntimeService } from "./src/service.js";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/acpx";
|
||||
|
||||
export const ACPX_PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const;
|
||||
export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { PluginLogger } from "openclaw/plugin-sdk";
|
||||
import type { PluginLogger } from "openclaw/plugin-sdk/acpx";
|
||||
import { ACPX_PINNED_VERSION, ACPX_PLUGIN_ROOT, buildAcpxLocalInstallCommand } from "./config.js";
|
||||
import {
|
||||
resolveSpawnFailure,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "openclaw/plugin-sdk";
|
||||
import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "openclaw/plugin-sdk/acpx";
|
||||
import {
|
||||
asOptionalBoolean,
|
||||
asOptionalString,
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createWindowsCmdShimFixture } from "../../../shared/windows-cmd-shim-test-fixtures.js";
|
||||
import { resolveSpawnCommand, type SpawnCommandCache } from "./process.js";
|
||||
import {
|
||||
resolveSpawnCommand,
|
||||
spawnAndCollect,
|
||||
type SpawnCommandCache,
|
||||
waitForExit,
|
||||
} from "./process.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
@@ -225,3 +231,62 @@ describe("resolveSpawnCommand", () => {
|
||||
expect(second.args[0]).toBe(scriptPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("waitForExit", () => {
|
||||
it("resolves when the child already exited before waiting starts", async () => {
|
||||
const child = spawn(process.execPath, ["-e", "process.exit(0)"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
child.once("close", () => {
|
||||
resolve();
|
||||
});
|
||||
child.once("error", reject);
|
||||
});
|
||||
|
||||
const exit = await waitForExit(child);
|
||||
expect(exit.code).toBe(0);
|
||||
expect(exit.signal).toBeNull();
|
||||
expect(exit.error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("spawnAndCollect", () => {
|
||||
it("returns abort error immediately when signal is already aborted", async () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
const result = await spawnAndCollect(
|
||||
{
|
||||
command: process.execPath,
|
||||
args: ["-e", "process.exit(0)"],
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
undefined,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
expect(result.code).toBeNull();
|
||||
expect(result.error?.name).toBe("AbortError");
|
||||
});
|
||||
|
||||
it("terminates a running process when signal aborts", async () => {
|
||||
const controller = new AbortController();
|
||||
const resultPromise = spawnAndCollect(
|
||||
{
|
||||
command: process.execPath,
|
||||
args: ["-e", "setTimeout(() => process.stdout.write('done'), 10_000)"],
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
undefined,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 10);
|
||||
|
||||
const result = await resultPromise;
|
||||
expect(result.error?.name).toBe("AbortError");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,12 +4,12 @@ import type {
|
||||
WindowsSpawnProgram,
|
||||
WindowsSpawnProgramCandidate,
|
||||
WindowsSpawnResolution,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/acpx";
|
||||
import {
|
||||
applyWindowsSpawnProgramPolicy,
|
||||
materializeWindowsSpawnProgram,
|
||||
resolveWindowsSpawnProgramCandidate,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/acpx";
|
||||
|
||||
export type SpawnExit = {
|
||||
code: number | null;
|
||||
@@ -114,6 +114,12 @@ export function resolveSpawnCommand(
|
||||
};
|
||||
}
|
||||
|
||||
function createAbortError(): Error {
|
||||
const error = new Error("Operation aborted.");
|
||||
error.name = "AbortError";
|
||||
return error;
|
||||
}
|
||||
|
||||
export function spawnWithResolvedCommand(
|
||||
params: {
|
||||
command: string;
|
||||
@@ -140,6 +146,15 @@ export function spawnWithResolvedCommand(
|
||||
}
|
||||
|
||||
export async function waitForExit(child: ChildProcessWithoutNullStreams): Promise<SpawnExit> {
|
||||
// Handle callers that start waiting after the child has already exited.
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
return {
|
||||
code: child.exitCode,
|
||||
signal: child.signalCode,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
return await new Promise<SpawnExit>((resolve) => {
|
||||
let settled = false;
|
||||
const finish = (result: SpawnExit) => {
|
||||
@@ -167,12 +182,23 @@ export async function spawnAndCollect(
|
||||
cwd: string;
|
||||
},
|
||||
options?: SpawnCommandOptions,
|
||||
runtime?: {
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
): Promise<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number | null;
|
||||
error: Error | null;
|
||||
}> {
|
||||
if (runtime?.signal?.aborted) {
|
||||
return {
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: null,
|
||||
error: createAbortError(),
|
||||
};
|
||||
}
|
||||
const child = spawnWithResolvedCommand(params, options);
|
||||
child.stdin.end();
|
||||
|
||||
@@ -185,13 +211,43 @@ export async function spawnAndCollect(
|
||||
stderr += String(chunk);
|
||||
});
|
||||
|
||||
const exit = await waitForExit(child);
|
||||
return {
|
||||
stdout,
|
||||
stderr,
|
||||
code: exit.code,
|
||||
error: exit.error,
|
||||
let abortKillTimer: NodeJS.Timeout | undefined;
|
||||
let aborted = false;
|
||||
const onAbort = () => {
|
||||
aborted = true;
|
||||
try {
|
||||
child.kill("SIGTERM");
|
||||
} catch {
|
||||
// Ignore kill races when child already exited.
|
||||
}
|
||||
abortKillTimer = setTimeout(() => {
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
child.kill("SIGKILL");
|
||||
} catch {
|
||||
// Ignore kill races when child already exited.
|
||||
}
|
||||
}, 250);
|
||||
abortKillTimer.unref?.();
|
||||
};
|
||||
runtime?.signal?.addEventListener("abort", onAbort, { once: true });
|
||||
|
||||
try {
|
||||
const exit = await waitForExit(child);
|
||||
return {
|
||||
stdout,
|
||||
stderr,
|
||||
code: exit.code,
|
||||
error: aborted ? createAbortError() : exit.error,
|
||||
};
|
||||
} finally {
|
||||
runtime?.signal?.removeEventListener("abort", onAbort);
|
||||
if (abortKillTimer) {
|
||||
clearTimeout(abortKillTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveSpawnFailure(
|
||||
|
||||
@@ -75,14 +75,35 @@ const setValue = command === "set" ? String(args[commandIndex + 2] || "") : "";
|
||||
|
||||
if (command === "sessions" && args[commandIndex + 1] === "ensure") {
|
||||
writeLog({ kind: "ensure", agent, args, sessionName: ensureName });
|
||||
emitJson({
|
||||
action: "session_ensured",
|
||||
acpxRecordId: "rec-" + ensureName,
|
||||
acpxSessionId: "sid-" + ensureName,
|
||||
agentSessionId: "inner-" + ensureName,
|
||||
name: ensureName,
|
||||
created: true,
|
||||
});
|
||||
if (process.env.MOCK_ACPX_ENSURE_EMPTY === "1") {
|
||||
emitJson({ action: "session_ensured", name: ensureName });
|
||||
} else {
|
||||
emitJson({
|
||||
action: "session_ensured",
|
||||
acpxRecordId: "rec-" + ensureName,
|
||||
acpxSessionId: "sid-" + ensureName,
|
||||
agentSessionId: "inner-" + ensureName,
|
||||
name: ensureName,
|
||||
created: true,
|
||||
});
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (command === "sessions" && args[commandIndex + 1] === "new") {
|
||||
writeLog({ kind: "new", agent, args, sessionName: ensureName });
|
||||
if (process.env.MOCK_ACPX_NEW_EMPTY === "1") {
|
||||
emitJson({ action: "session_created", name: ensureName });
|
||||
} else {
|
||||
emitJson({
|
||||
action: "session_created",
|
||||
acpxRecordId: "rec-" + ensureName,
|
||||
acpxSessionId: "sid-" + ensureName,
|
||||
agentSessionId: "inner-" + ensureName,
|
||||
name: ensureName,
|
||||
created: true,
|
||||
});
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -377,4 +377,51 @@ describe("AcpxRuntime", () => {
|
||||
expect(report.code).toBe("ACP_BACKEND_UNAVAILABLE");
|
||||
expect(report.installCommand).toContain("acpx");
|
||||
});
|
||||
|
||||
it("falls back to 'sessions new' when 'sessions ensure' returns no session IDs", async () => {
|
||||
process.env.MOCK_ACPX_ENSURE_EMPTY = "1";
|
||||
try {
|
||||
const { runtime, logPath } = await createMockRuntimeFixture();
|
||||
const handle = await runtime.ensureSession({
|
||||
sessionKey: "agent:claude:acp:fallback-test",
|
||||
agent: "claude",
|
||||
mode: "persistent",
|
||||
});
|
||||
expect(handle.backend).toBe("acpx");
|
||||
expect(handle.acpxRecordId).toBe("rec-agent:claude:acp:fallback-test");
|
||||
expect(handle.agentSessionId).toBe("inner-agent:claude:acp:fallback-test");
|
||||
|
||||
const logs = await readMockRuntimeLogEntries(logPath);
|
||||
expect(logs.some((entry) => entry.kind === "ensure")).toBe(true);
|
||||
expect(logs.some((entry) => entry.kind === "new")).toBe(true);
|
||||
} finally {
|
||||
delete process.env.MOCK_ACPX_ENSURE_EMPTY;
|
||||
}
|
||||
});
|
||||
|
||||
it("fails with ACP_SESSION_INIT_FAILED when both ensure and new omit session IDs", async () => {
|
||||
process.env.MOCK_ACPX_ENSURE_EMPTY = "1";
|
||||
process.env.MOCK_ACPX_NEW_EMPTY = "1";
|
||||
try {
|
||||
const { runtime, logPath } = await createMockRuntimeFixture();
|
||||
|
||||
await expect(
|
||||
runtime.ensureSession({
|
||||
sessionKey: "agent:claude:acp:fallback-fail",
|
||||
agent: "claude",
|
||||
mode: "persistent",
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "ACP_SESSION_INIT_FAILED",
|
||||
message: expect.stringContaining("neither 'sessions ensure' nor 'sessions new'"),
|
||||
});
|
||||
|
||||
const logs = await readMockRuntimeLogEntries(logPath);
|
||||
expect(logs.some((entry) => entry.kind === "ensure")).toBe(true);
|
||||
expect(logs.some((entry) => entry.kind === "new")).toBe(true);
|
||||
} finally {
|
||||
delete process.env.MOCK_ACPX_ENSURE_EMPTY;
|
||||
delete process.env.MOCK_ACPX_NEW_EMPTY;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,8 +10,8 @@ import type {
|
||||
AcpRuntimeStatus,
|
||||
AcpRuntimeTurnInput,
|
||||
PluginLogger,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import { AcpRuntimeError } from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/acpx";
|
||||
import { AcpRuntimeError } from "openclaw/plugin-sdk/acpx";
|
||||
import { type ResolvedAcpxPluginConfig } from "./config.js";
|
||||
import { checkAcpxVersion } from "./ensure.js";
|
||||
import {
|
||||
@@ -179,7 +179,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
const cwd = asTrimmedString(input.cwd) || this.config.cwd;
|
||||
const mode = input.mode;
|
||||
|
||||
const events = await this.runControlCommand({
|
||||
let events = await this.runControlCommand({
|
||||
args: this.buildControlArgs({
|
||||
cwd,
|
||||
command: [agent, "sessions", "ensure", "--name", sessionName],
|
||||
@@ -187,12 +187,36 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
cwd,
|
||||
fallbackCode: "ACP_SESSION_INIT_FAILED",
|
||||
});
|
||||
const ensuredEvent = events.find(
|
||||
let ensuredEvent = events.find(
|
||||
(event) =>
|
||||
asOptionalString(event.agentSessionId) ||
|
||||
asOptionalString(event.acpxSessionId) ||
|
||||
asOptionalString(event.acpxRecordId),
|
||||
);
|
||||
|
||||
if (!ensuredEvent) {
|
||||
events = await this.runControlCommand({
|
||||
args: this.buildControlArgs({
|
||||
cwd,
|
||||
command: [agent, "sessions", "new", "--name", sessionName],
|
||||
}),
|
||||
cwd,
|
||||
fallbackCode: "ACP_SESSION_INIT_FAILED",
|
||||
});
|
||||
ensuredEvent = events.find(
|
||||
(event) =>
|
||||
asOptionalString(event.agentSessionId) ||
|
||||
asOptionalString(event.acpxSessionId) ||
|
||||
asOptionalString(event.acpxRecordId),
|
||||
);
|
||||
if (!ensuredEvent) {
|
||||
throw new AcpRuntimeError(
|
||||
"ACP_SESSION_INIT_FAILED",
|
||||
`ACP session init failed: neither 'sessions ensure' nor 'sessions new' returned valid session identifiers for ${sessionName}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const acpxRecordId = ensuredEvent ? asOptionalString(ensuredEvent.acpxRecordId) : undefined;
|
||||
const agentSessionId = ensuredEvent ? asOptionalString(ensuredEvent.agentSessionId) : undefined;
|
||||
const backendSessionId = ensuredEvent
|
||||
@@ -329,7 +353,10 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
return ACPX_CAPABILITIES;
|
||||
}
|
||||
|
||||
async getStatus(input: { handle: AcpRuntimeHandle }): Promise<AcpRuntimeStatus> {
|
||||
async getStatus(input: {
|
||||
handle: AcpRuntimeHandle;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<AcpRuntimeStatus> {
|
||||
const state = this.resolveHandleState(input.handle);
|
||||
const events = await this.runControlCommand({
|
||||
args: this.buildControlArgs({
|
||||
@@ -339,6 +366,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
cwd: state.cwd,
|
||||
fallbackCode: "ACP_TURN_FAILED",
|
||||
ignoreNoSession: true,
|
||||
signal: input.signal,
|
||||
});
|
||||
const detail = events.find((event) => !toAcpxErrorEvent(event)) ?? events[0];
|
||||
if (!detail) {
|
||||
@@ -562,6 +590,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
cwd: string;
|
||||
fallbackCode: AcpRuntimeErrorCode;
|
||||
ignoreNoSession?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<AcpxJsonObject[]> {
|
||||
const result = await spawnAndCollect(
|
||||
{
|
||||
@@ -570,6 +599,9 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
cwd: params.cwd,
|
||||
},
|
||||
this.spawnCommandOptions,
|
||||
{
|
||||
signal: params.signal,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AcpRuntime, OpenClawPluginServiceContext } from "openclaw/plugin-sdk";
|
||||
import type { AcpRuntime, OpenClawPluginServiceContext } from "openclaw/plugin-sdk/acpx";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AcpRuntimeError } from "../../../src/acp/runtime/errors.js";
|
||||
import {
|
||||
|
||||
@@ -3,8 +3,8 @@ import type {
|
||||
OpenClawPluginService,
|
||||
OpenClawPluginServiceContext,
|
||||
PluginLogger,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/acpx";
|
||||
import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk/acpx";
|
||||
import { resolveAcpxPluginConfig, type ResolvedAcpxPluginConfig } from "./config.js";
|
||||
import { ensureAcpx } from "./ensure.js";
|
||||
import { ACPX_BACKEND_ID, AcpxRuntime } from "./runtime.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { bluebubblesPlugin } from "./src/channel.js";
|
||||
import { setBlueBubblesRuntime } from "./src/runtime.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { resolveBlueBubblesAccount } from "./accounts.js";
|
||||
import { normalizeResolvedSecretInputString } from "./secret-input.js";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
||||
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { bluebubblesMessageActions } from "./actions.js";
|
||||
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
readStringParam,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelMessageActionName,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { resolveBlueBubblesAccount } from "./accounts.js";
|
||||
import { sendBlueBubblesAttachment } from "./attachments.js";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "./test-mocks.js";
|
||||
import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
||||
import { postMultipartFormData } from "./multipart.js";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { ChannelAccountSnapshot, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelPlugin,
|
||||
OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
buildChannelConfigSchema,
|
||||
@@ -13,7 +17,7 @@ import {
|
||||
resolveBlueBubblesGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
type ResolvedBlueBubblesAccount,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
||||
import { postMultipartFormData } from "./multipart.js";
|
||||
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk";
|
||||
import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { z } from "zod";
|
||||
import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
||||
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { sendBlueBubblesMedia } from "./media-send.js";
|
||||
import { setBlueBubblesRuntime } from "./runtime.js";
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { resolveBlueBubblesAccount } from "./accounts.js";
|
||||
import { sendBlueBubblesAttachment } from "./attachments.js";
|
||||
import { resolveBlueBubblesMessageId } from "./monitor.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import type { NormalizedWebhookMessage } from "./monitor-normalize.js";
|
||||
import type { BlueBubblesCoreRuntime, WebhookTarget } from "./monitor-shared.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import {
|
||||
DM_GROUP_ACCESS_REASON,
|
||||
createScopedPairingAccess,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
resolveControlCommandGate,
|
||||
stripMarkdown,
|
||||
type HistoryEntry,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { downloadBlueBubblesAttachment } from "./attachments.js";
|
||||
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
|
||||
import { fetchBlueBubblesHistory } from "./history.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { normalizeWebhookPath, type OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { normalizeWebhookPath, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
import { getBlueBubblesRuntime } from "./runtime.js";
|
||||
import type { BlueBubblesAccountConfig } from "./types.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
||||
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
readWebhookBodyOrReject,
|
||||
resolveWebhookTargetWithAuthOrRejectSync,
|
||||
resolveWebhookTargets,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js";
|
||||
import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js";
|
||||
import { logVerbose, processMessage, processReaction } from "./monitor-processing.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
||||
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { WizardPrompter } from "openclaw/plugin-sdk";
|
||||
import type { WizardPrompter } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk", () => ({
|
||||
vi.mock("openclaw/plugin-sdk/bluebubbles", () => ({
|
||||
DEFAULT_ACCOUNT_ID: "default",
|
||||
addWildcardAllowFrom: vi.fn(),
|
||||
formatDocsLink: (_url: string, fallback: string) => fallback,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
OpenClawConfig,
|
||||
DmPolicy,
|
||||
WizardPrompter,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
addWildcardAllowFrom,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
mergeAllowFromEntries,
|
||||
normalizeAccountId,
|
||||
promptAccountId,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
resolveBlueBubblesAccount,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BaseProbeResult } from "openclaw/plugin-sdk";
|
||||
import type { BaseProbeResult } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { normalizeSecretInputString } from "./secret-input.js";
|
||||
import { buildBlueBubblesApiUrl, blueBubblesFetchWithTimeout } from "./types.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
||||
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
||||
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
||||
|
||||
let runtime: PluginRuntime | null = null;
|
||||
type LegacyRuntimeLogShape = { log?: (message: string) => void };
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
normalizeSecretInputString,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { z } from "zod";
|
||||
|
||||
export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "./test-mocks.js";
|
||||
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { stripMarkdown } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { stripMarkdown } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { resolveBlueBubblesAccount } from "./accounts.js";
|
||||
import {
|
||||
getCachedBlueBubblesPrivateApiStatus,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type ParsedChatTarget,
|
||||
resolveServicePrefixedAllowTarget,
|
||||
resolveServicePrefixedTarget,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
|
||||
export type BlueBubblesService = "imessage" | "sms" | "auto";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk";
|
||||
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/bluebubbles";
|
||||
|
||||
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk";
|
||||
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/bluebubbles";
|
||||
|
||||
export type BlueBubblesGroupConfig = {
|
||||
/** If true, only respond in this group when mentioned. */
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthContext,
|
||||
type ProviderAuthResult,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
} from "openclaw/plugin-sdk/copilot-proxy";
|
||||
|
||||
const DEFAULT_BASE_URL = "http://localhost:3000/v1";
|
||||
const DEFAULT_API_KEY = "n/a";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import os from "node:os";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/device-pair";
|
||||
import {
|
||||
approveDevicePairing,
|
||||
listDevicePairing,
|
||||
resolveGatewayBindUrl,
|
||||
runPluginCommandWithTimeout,
|
||||
resolveTailnetHostWithRunner,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
} from "openclaw/plugin-sdk/device-pair";
|
||||
import qrcode from "qrcode-terminal";
|
||||
import {
|
||||
armPairNotifyOnce,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { listDevicePairing } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/device-pair";
|
||||
import { listDevicePairing } from "openclaw/plugin-sdk/device-pair";
|
||||
|
||||
const NOTIFY_STATE_FILE = "device-pair-notify.json";
|
||||
const NOTIFY_POLL_INTERVAL_MS = 10_000;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diagnostics-otel";
|
||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/diagnostics-otel";
|
||||
import { createDiagnosticsOtelService } from "./src/service.js";
|
||||
|
||||
const plugin = {
|
||||
|
||||
@@ -98,16 +98,18 @@ vi.mock("@opentelemetry/semantic-conventions", () => ({
|
||||
ATTR_SERVICE_NAME: "service.name",
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk")>("openclaw/plugin-sdk");
|
||||
vi.mock("openclaw/plugin-sdk/diagnostics-otel", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/diagnostics-otel")>(
|
||||
"openclaw/plugin-sdk/diagnostics-otel",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
registerLogTransport: registerLogTransportMock,
|
||||
};
|
||||
});
|
||||
|
||||
import type { OpenClawPluginServiceContext } from "openclaw/plugin-sdk";
|
||||
import { emitDiagnosticEvent } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginServiceContext } from "openclaw/plugin-sdk/diagnostics-otel";
|
||||
import { emitDiagnosticEvent } from "openclaw/plugin-sdk/diagnostics-otel";
|
||||
import { createDiagnosticsOtelService } from "./service.js";
|
||||
|
||||
const OTEL_TEST_STATE_DIR = "/tmp/openclaw-diagnostics-otel-test";
|
||||
|
||||
@@ -9,8 +9,15 @@ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
||||
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||
import { ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
|
||||
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
||||
import type { DiagnosticEventPayload, OpenClawPluginService } from "openclaw/plugin-sdk";
|
||||
import { onDiagnosticEvent, redactSensitiveText, registerLogTransport } from "openclaw/plugin-sdk";
|
||||
import type {
|
||||
DiagnosticEventPayload,
|
||||
OpenClawPluginService,
|
||||
} from "openclaw/plugin-sdk/diagnostics-otel";
|
||||
import {
|
||||
onDiagnosticEvent,
|
||||
redactSensitiveText,
|
||||
registerLogTransport,
|
||||
} from "openclaw/plugin-sdk/diagnostics-otel";
|
||||
|
||||
const DEFAULT_SERVICE_NAME = "openclaw";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs";
|
||||
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/diffs";
|
||||
import {
|
||||
diffsPluginConfigSchema,
|
||||
resolveDiffsPluginDefaults,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/diffs";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { launchMock } = vi.hoisted(() => ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/diffs";
|
||||
import { chromium } from "playwright-core";
|
||||
import type { DiffRenderOptions, DiffTheme } from "./types.js";
|
||||
import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/diffs";
|
||||
import {
|
||||
DIFF_IMAGE_QUALITY_PRESETS,
|
||||
DIFF_INDICATORS,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { PluginLogger } from "openclaw/plugin-sdk";
|
||||
import type { PluginLogger } from "openclaw/plugin-sdk/diffs";
|
||||
import type { DiffArtifactStore } from "./store.js";
|
||||
import { DIFF_ARTIFACT_ID_PATTERN, DIFF_ARTIFACT_TOKEN_PATTERN } from "./types.js";
|
||||
import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { PluginLogger } from "openclaw/plugin-sdk";
|
||||
import type { PluginLogger } from "openclaw/plugin-sdk/diffs";
|
||||
import type { DiffArtifactMeta, DiffOutputFormat } from "./types.js";
|
||||
|
||||
const DEFAULT_TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DiffScreenshotter } from "./browser.js";
|
||||
import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/diffs";
|
||||
import { PlaywrightDiffScreenshotter, type DiffScreenshotter } from "./browser.js";
|
||||
import { resolveDiffImageRenderOptions } from "./config.js";
|
||||
import { renderDiffDocument } from "./render.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/diffs";
|
||||
|
||||
const DEFAULT_GATEWAY_PORT = 18789;
|
||||
|
||||
|
||||
@@ -302,10 +302,11 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
textChunkLimit: 2000,
|
||||
pollMaxOptions: 10,
|
||||
resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, silent }) => {
|
||||
sendText: async ({ cfg, to, text, accountId, deps, replyToId, silent }) => {
|
||||
const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
|
||||
const result = await send(to, text, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
replyTo: replyToId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
silent: silent ?? undefined,
|
||||
@@ -313,6 +314,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
return { channel: "discord", ...result };
|
||||
},
|
||||
sendMedia: async ({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
@@ -325,6 +327,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
|
||||
const result = await send(to, text, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
mediaUrl,
|
||||
mediaLocalRoots,
|
||||
replyTo: replyToId ?? undefined,
|
||||
@@ -333,8 +336,9 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
});
|
||||
return { channel: "discord", ...result };
|
||||
},
|
||||
sendPoll: async ({ to, poll, accountId, silent }) =>
|
||||
sendPoll: async ({ cfg, to, poll, accountId, silent }) =>
|
||||
await getDiscordRuntime().channel.discord.sendPollDiscord(to, poll, {
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
silent: silent ?? undefined,
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/feishu";
|
||||
import { registerFeishuBitableTools } from "./src/bitable.js";
|
||||
import { feishuPlugin } from "./src/channel.js";
|
||||
import { registerFeishuChatTools } from "./src/chat.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
|
||||
import type {
|
||||
FeishuConfig,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { createFeishuToolClient } from "./tool-account.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
||||
import type { FeishuMessageEvent } from "./bot.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
||||
import {
|
||||
buildAgentMediaPayload,
|
||||
buildPendingHistoryContextFromMap,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
resolveOpenProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/feishu";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { tryRecordMessage, tryRecordMessagePersistent } from "./dedup.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import {
|
||||
buildBaseChannelStatusSummary,
|
||||
createDefaultChannelRuntimeState,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/feishu";
|
||||
import {
|
||||
resolveFeishuAccount,
|
||||
resolveFeishuCredentials,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { FeishuChatSchema, type FeishuChatParams } from "./chat-schema.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
createDedupeCache,
|
||||
createPersistentDedupe,
|
||||
readJsonFileWithFallback,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/feishu";
|
||||
|
||||
// Persistent TTL: 24 hours — survives restarts & WebSocket reconnects.
|
||||
const DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { normalizeFeishuTarget } from "./targets.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { registerFeishuDocTools } from "./docx.js";
|
||||
import { createToolFactoryHarness } from "./tool-factory-test-harness.js";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isAbsolute } from "node:path";
|
||||
import { basename } from "node:path";
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
|
||||
import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
|
||||
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/feishu";
|
||||
import type { DynamicAgentCreationConfig } from "./types.js";
|
||||
|
||||
export type MaybeCreateDynamicAgentResult = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { Readable } from "stream";
|
||||
import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as crypto from "crypto";
|
||||
import * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk/feishu";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { raceWithTimeoutAndAbort } from "./async.js";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { hasControlCommand } from "../../../src/auto-reply/command-detection.js";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
||||
import { probeFeishu } from "./probe.js";
|
||||
import type { ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type RuntimeEnv,
|
||||
WEBHOOK_ANOMALY_COUNTER_DEFAULTS as WEBHOOK_ANOMALY_COUNTER_DEFAULTS_FROM_SDK,
|
||||
WEBHOOK_RATE_LIMIT_DEFAULTS as WEBHOOK_RATE_LIMIT_DEFAULTS_FROM_SDK,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/feishu";
|
||||
|
||||
export const wsClients = new Map<string, Lark.WSClient>();
|
||||
export const httpServers = new Map<string, http.Server>();
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
applyBasicWebhookRequestGuards,
|
||||
type RuntimeEnv,
|
||||
installRequestBodyLimitGuard,
|
||||
} from "openclaw/plugin-sdk";
|
||||
} from "openclaw/plugin-sdk/feishu";
|
||||
import { createFeishuWSClient } from "./client.js";
|
||||
import {
|
||||
botOpenIds,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
||||
import { listEnabledFeishuAccounts, resolveFeishuAccount } from "./accounts.js";
|
||||
import {
|
||||
monitorSingleAccount,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createServer } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { feishuOnboardingAdapter } from "./onboarding.js";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user