Compare commits

..

6 Commits

Author SHA1 Message Date
Shakker
09be5d45d5
Merge pull request #4651 from yuting0624/fix/status-command-line-crash
fix(line): resolve TypeError in status command when LINE is enabled
2026-01-30 15:41:40 +00:00
Yuting Lin
3fbf99d725 fix(line): resolve TypeError in status command 2026-01-30 15:41:22 +00:00
Ayush Ojha
37e295fc02
fix: don't warn about expired OAuth tokens with valid refresh tokens (#4593)
OAuth credentials with a refresh token auto-renew on first API call,
so the doctor should not warn about access token expiration when a
refresh token is present. This avoids unnecessary "expired" warnings
that prompt users to re-auth when no action is needed.

Fixes #3032

Co-authored-by: Ayush Ojha <ayushozha@outlook.com>
2026-01-30 15:39:17 +00:00
Ayaan Zaidi
da71eaebd2 fix: correct telegram html nesting (#4578) (thanks @ThanhNguyxn) 2026-01-30 16:53:39 +05:30
ThanhNguyxn
8e5a684445 style: format test file 2026-01-30 16:53:39 +05:30
ThanhNguyxn
b05d57964b fix(telegram): properly nest overlapping HTML tags (#4071)
Unify style and link closing in render.ts to use LIFO order across
both element types, fixing cases where bold/italic spans containing
autolinks produced invalid HTML like <b><a></b></a>.
2026-01-30 16:53:39 +05:30
3 changed files with 62 additions and 17 deletions

View File

@ -4,6 +4,7 @@ import {
LineConfigSchema,
processLineMessage,
type ChannelPlugin,
type ChannelStatusIssue,
type OpenClawConfig,
type LineConfig,
type LineChannelData,
@ -560,19 +561,26 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
lastStopAt: null,
lastError: null,
},
collectStatusIssues: ({ account }) => {
const issues: Array<{ level: "error" | "warning"; message: string }> = [];
if (!account.channelAccessToken?.trim()) {
issues.push({
level: "error",
message: "LINE channel access token not configured",
});
}
if (!account.channelSecret?.trim()) {
issues.push({
level: "error",
message: "LINE channel secret not configured",
});
collectStatusIssues: (accounts) => {
const issues: ChannelStatusIssue[] = [];
for (const account of accounts) {
const accountId = account.accountId ?? DEFAULT_ACCOUNT_ID;
if (!account.channelAccessToken?.trim()) {
issues.push({
channel: "line",
accountId,
kind: "config",
message: "LINE channel access token not configured",
});
}
if (!account.channelSecret?.trim()) {
issues.push({
channel: "line",
accountId,
kind: "config",
message: "LINE channel secret not configured",
});
}
}
return issues;
},

View File

@ -52,11 +52,39 @@ describe("buildAuthHealthSummary", () => {
);
expect(statuses["anthropic:ok"]).toBe("ok");
expect(statuses["anthropic:expiring"]).toBe("expiring");
expect(statuses["anthropic:expired"]).toBe("expired");
// OAuth credentials with refresh tokens are auto-renewable, so they report "ok"
expect(statuses["anthropic:expiring"]).toBe("ok");
expect(statuses["anthropic:expired"]).toBe("ok");
expect(statuses["anthropic:api"]).toBe("static");
const provider = summary.providers.find((entry) => entry.provider === "anthropic");
expect(provider?.status).toBe("expired");
expect(provider?.status).toBe("ok");
});
it("reports expired for OAuth without a refresh token", () => {
vi.spyOn(Date, "now").mockReturnValue(now);
const store = {
version: 1,
profiles: {
"google:no-refresh": {
type: "oauth" as const,
provider: "google-antigravity",
access: "access",
refresh: "",
expires: now - 10_000,
},
},
};
const summary = buildAuthHealthSummary({
store,
warnAfterMs: DEFAULT_OAUTH_WARN_MS,
});
const statuses = Object.fromEntries(
summary.profiles.map((profile) => [profile.profileId, profile.status]),
);
expect(statuses["google:no-refresh"]).toBe("expired");
});
});

View File

@ -123,7 +123,16 @@ function buildProfileHealth(params: {
};
}
const { status, remainingMs } = resolveOAuthStatus(credential.expires, now, warnAfterMs);
const hasRefreshToken = typeof credential.refresh === "string" && credential.refresh.length > 0;
const { status: rawStatus, remainingMs } = resolveOAuthStatus(
credential.expires,
now,
warnAfterMs,
);
// OAuth credentials with a valid refresh token auto-renew on first API call,
// so don't warn about access token expiration.
const status =
hasRefreshToken && (rawStatus === "expired" || rawStatus === "expiring") ? "ok" : rawStatus;
return {
profileId,
provider: credential.provider,