feat(desktop): add tooltip provider and model management

This commit is contained in:
leokun
2026-09-04 21:58:24 +08:00
parent 37f9c4c4c8
commit 4700d3abc0
25 changed files with 717 additions and 262 deletions
@@ -13,9 +13,6 @@ import {
SCOPES,
} from "./google_oauth.ts";
export const CALLBACK_PORT = 51121;
export const CALLBACK_PATH = "/oauth-callback";
const AUTHORIZATION_LIFETIME_MS = 5 * 60 * 1000;
type Session = { createdAtMs: number };
@@ -151,7 +148,6 @@ export const antigravityAuthorizationCodeOAuth: OAuth2AuthorizationCodeAddMethod
"en-US": "Authorize Antigravity with your Google Account for Gemini and Claude models.",
"zh-CN": "使用 Google 账号完成 Antigravity 授权,以使用 Gemini 与 Claude 模型。",
},
callback: { port: CALLBACK_PORT, path: CALLBACK_PATH },
begin,
complete,
};
@@ -33,6 +33,10 @@ function context(requests: Array<{ url: string; body?: string }>): PluginContext
}
Deno.test("authorization URL uses Core-owned state, callback, and PKCE challenge", async () => {
assert(
antigravityAuthorizationCodeOAuth.callback?.port === undefined,
"Antigravity must let Core allocate an available loopback port",
);
const result = await antigravityAuthorizationCodeOAuth.begin(
{
redirectUri: "http://127.0.0.1:51121/oauth-callback",
@@ -2,8 +2,8 @@
"apiVersion": 1,
"id": "dev.cursorbyok.plugins.antigravity-auth",
"name": "Antigravity",
"version": "0.3.0",
"author": "Antigravity",
"version": "0.3.1",
"author": "@masudranaxpert",
"minAppVersion": "0.1.0",
"icon": "assets/antigravity.svg",
"entry": "main.ts",
+4
View File
@@ -177,6 +177,10 @@ pub fn api_router(service: ControlService) -> Router {
"/__byok-api__/api/plugins/{plugin_id}/providers/{provider_id}/models/sync",
post(plugins::sync_models),
)
.route(
"/__byok-api__/api/plugins/{plugin_id}/providers/{provider_id}/models/enabled",
put(plugins::set_model_enabled),
)
.route(
"/__byok-api__/api/settings/observability",
get(settings::get).put(settings::update),
+19
View File
@@ -105,6 +105,25 @@ pub async fn sync_models(
Ok(Json(serde_json::json!({ "models": count })))
}
pub async fn set_model_enabled(
State(service): State<ControlService>,
Path((plugin_id, provider_id)): Path<(String, String)>,
Json(input): Json<serde_json::Value>,
) -> Result<StatusCode> {
let model_id = input
.get("modelId")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| crate::Error::Config("modelId must be a string".into()))?;
let enabled = input
.get("enabled")
.and_then(serde_json::Value::as_bool)
.ok_or_else(|| crate::Error::Config("enabled must be a boolean".into()))?;
service
.plugin_set_model_enabled(&plugin_id, &provider_id, model_id, enabled)
.await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn runtime_status(
State(service): State<ControlService>,
) -> Result<Json<PluginRuntimeStatus>> {
+12
View File
@@ -241,6 +241,18 @@ impl ControlService {
self.plugins.sync_models(plugin_id, provider_id).await
}
pub async fn plugin_set_model_enabled(
&self,
plugin_id: &str,
provider_id: &str,
model_id: &str,
enabled: bool,
) -> Result<()> {
self.plugins
.set_model_enabled(plugin_id, provider_id, model_id, enabled)
.await
}
pub async fn remove_plugin_configuration(&self, plugin_id: &str) -> Result<()> {
self.plugins.remove(plugin_id).await
}
+5 -1
View File
@@ -41,6 +41,7 @@ const DIFF_SINGLE_LIMIT: usize = 16_000;
const PREVIOUS_COMMIT_LIMIT: usize = 12;
const EXPLICIT_CONTEXT_LIMIT: usize = 20_000;
const GENERATION_TIMEOUT: Duration = Duration::from_secs(180);
const COMMIT_MAX_OUTPUT_TOKENS: u64 = 30_000;
pub async fn write_git_commit_message(
State(registry): State<TransportRegistry>,
@@ -142,7 +143,10 @@ fn build_invocation(
instructions: settings.effective_prompt().to_owned(),
tools: Vec::new(),
},
model: ModelSpec::new(model_id.to_owned()),
model: ModelSpec {
max_output_tokens: Some(COMMIT_MAX_OUTPUT_TOKENS),
..ModelSpec::new(model_id.to_owned())
},
history: vec![ProjectedMessage {
message_id: "commit-message".into(),
role: Role::User,
@@ -831,6 +831,7 @@ mod tests {
provider_type: "test".into(),
max_output_tokens: None,
images: false,
enabled: true,
});
assert_eq!(details.model_id, "plugin:test/provider/model");
let agent::model_details::Credentials::ApiKeyCredentials(credentials) =
+2
View File
@@ -121,6 +121,7 @@ pub struct PluginModelDescriptor {
pub provider_type: String,
pub max_output_tokens: Option<u64>,
pub images: bool,
pub enabled: bool,
}
#[derive(Clone, Debug, Serialize)]
@@ -221,6 +222,7 @@ impl PluginModelDescriptor {
provider_type: provider.provider_type.clone(),
max_output_tokens: model.max_output_tokens,
images: model.images,
enabled: model.enabled,
}
}
}
+29 -1
View File
@@ -189,7 +189,7 @@ impl PluginRegistry {
.models(&entry.manifest.id, &provider.id)
.await
.unwrap_or_default();
models.extend(stored.iter().map(|model| {
models.extend(stored.iter().filter(|model| model.enabled).map(|model| {
PluginModelDescriptor::new(
&entry.manifest.id,
&entry.manifest.name,
@@ -326,6 +326,13 @@ impl PluginRegistry {
"plugin '{plugin_id}' does not define OAuth method '{method_id}'"
))
})?;
// 同一添加入口只有一个活跃生命周期。重新开始时先丢弃旧会话,
// Drop 授权码会话中的 CallbackHandle 会立即释放 loopback listener。
self.inner.oauth_sessions.lock().await.retain(|_, session| {
session.plugin_id != plugin_id
|| session.resource_type != resource_type
|| session.method_id != method_id
});
let worker = self.worker(&entry, &executable).await;
let session_id = uuid::Uuid::new_v4().to_string();
@@ -876,6 +883,27 @@ impl PluginRegistry {
.await
}
pub async fn set_model_enabled(
&self,
plugin_id: &str,
provider_id: &str,
model_id: &str,
enabled: bool,
) -> Result<()> {
let executable = self.executable()?;
let entry = self.find_entry(&executable, plugin_id).await?;
let provider = find_provider(&entry, provider_id)?;
if !provider.has_models {
return Err(Error::Config(format!(
"plugin provider '{provider_id}' does not enumerate models"
)));
}
self.inner
.state
.set_model_enabled(plugin_id, provider_id, model_id, enabled)
.await
}
pub async fn remove(&self, plugin_id: &str) -> Result<()> {
if let Some(worker) = self.inner.workers.lock().await.remove(plugin_id) {
worker.stop().await;
+40
View File
@@ -138,10 +138,16 @@ pub struct StoredModel {
pub max_output_tokens: Option<u64>,
#[serde(default)]
pub images: bool,
#[serde(default = "default_model_enabled")]
pub enabled: bool,
#[serde(default)]
pub private_data: serde_json::Value,
}
fn default_model_enabled() -> bool {
true
}
impl StoredModel {
pub fn from_definition(value: &serde_json::Value) -> Result<Self> {
let object = value
@@ -179,6 +185,7 @@ impl StoredModel {
.get("maxOutputTokens")
.and_then(serde_json::Value::as_u64),
images: capability("images"),
enabled: true,
private_data: object
.get("privateData")
.cloned()
@@ -328,6 +335,39 @@ impl PluginStateStore {
provider_id: &str,
models: &[StoredModel],
) -> Result<()> {
let previous = self.models(plugin_id, provider_id).await?;
let models = models
.iter()
.cloned()
.map(|mut model| {
if let Some(old) = previous.iter().find(|old| old.id == model.id) {
model.enabled = old.enabled;
}
model
})
.collect::<Vec<_>>();
self.data
.update(
plugin_id,
&model_key(provider_id),
&serde_json::to_value(models)?,
)
.await
}
pub async fn set_model_enabled(
&self,
plugin_id: &str,
provider_id: &str,
model_id: &str,
enabled: bool,
) -> Result<()> {
let mut models = self.models(plugin_id, provider_id).await?;
let model = models
.iter_mut()
.find(|model| model.id == model_id)
.ok_or_else(|| Error::RunNotFound(format!("plugin model {model_id}")))?;
model.enabled = enabled;
self.data
.update(
plugin_id,